关键词搜索

源码搜索 ×
×

await,async 我要把它翻个底朝天,这回你总该明白了吧

发布2021-02-22浏览296次

详情内容

一:背景

  1. 讲故事
    await,async 这玩意的知识点已经被人说的烂的不能再烂了,看似没什么好说的,但我发现有不少文章还是从理论上讲述了这两个语法糖的用法,懂得还是懂,不懂的看似懂了过几天又不懂了,人生如戏全靠记是不行的哈???,其实本质上来说 await, async 只是编译器层面上的语法糖,在 IL 层面都会被打成原型的,所以在这个层面上认识这两个语法糖是非常有必要的。

二:从 IL 层面认识

  1. 使用 WebClient 下载
    为了方便打回原型,我先上一个例子,使用 webclient 异步下载 http://cnblogs.com 的html,代码如下:
class Program
{
    static void Main(string[] args)
    {
        var html = GetResult();

        Console.WriteLine("稍等... 正在下载 cnblogs -> html \r\n");

        var content = html.Result;

        Console.WriteLine(content);
    }

    static async Task<string> GetResult()
    {
        var client = new WebClient();

        var content = await client.DownloadStringTaskAsync(new Uri("http://cnblogs.com"));

        return content;
    }
}

    上面的代码非常简单,可以看到异步操作没有阻塞主线程输出: 稍等… 正在下载 cnblogs -> html \r\n, 编译器层面没什么好说的 ,接下来看下在 IL 层面发生了什么?

    1. 挖掘 await async 的IL代码
      还是老规矩, ilSpy 走起,如下图:

    在这里插入图片描述

    可以看到,这里有一个 GetResult 方法 ,一个 Main 方法,还有一个不知道在哪里冒出来的 d__1 类,接下来和大家一个一个聊。
    在这里插入图片描述

    <1> d__1> 类
    因为不知道从哪里冒出来的,特别引人关注,所以看看它的 IL 是咋样的?

    .class nested private auto ansi sealed beforefieldinit '<GetResult>d__1'
    	extends [System.Runtime]System.Object
    	implements [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine
    {
    	.method private final hidebysig newslot virtual 
    		instance void MoveNext () cil managed 
    	{
        }
    
        .method private final hidebysig newslot virtual 
    		instance void SetStateMachine (
    			class [System.Runtime]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine
    		) cil managed 
        {
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    从上面的 IL 代码可以看到,这是自动生成的 d__1 类实现了接口 IAsyncStateMachine,定义如下:

    在这里插入图片描述

    看到里面的 MoveNext 是不是很眼熟,平时你在 foreach 集合的时候就会用到这个方法,那时人家叫做枚举类,在这里算是被改造了一下, 叫状态机???。

    <2> GetResult ()
    为了方便演示,我对方法体中的 IL 代码做一下简化:

    .method private hidebysig static 
    	class [System.Runtime]System.Threading.Tasks.Task`1<string> GetResult () cil managed 
    {
    	IL_0000: newobj instance void ConsoleApp3.Program/'<GetResult>d__1'::.ctor()
    	IL_0005: stloc.0
    	IL_0006: ldloc.0
    	IL_0007: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::Create()
    	IL_000c: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
    	IL_0011: ldloc.0
    	IL_0012: ldc.i4.m1
    	IL_0013: stfld int32 ConsoleApp3.Program/'<GetResult>d__1'::'<>1__state'
    	IL_0018: ldloc.0
    	IL_0019: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
    	IL_001e: ldloca.s 0
    	IL_0020: call instance void valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::Start<class ConsoleApp3.Program/'<GetResult>d__1'>(!!0&)
    	IL_0025: ldloc.0
    	IL_0026: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string> ConsoleApp3.Program/'<GetResult>d__1'::'<>t__builder'
    	IL_002b: call instance class [System.Runtime]System.Threading.Tasks.Task`1<!0> valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::get_Task()
    	IL_0030: ret
    } // end of method Program::GetResult
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    如果你稍微懂一点的话,在 IL_0000 处的 newobj 你就应该知道这个方法就是做了 new d__1,然后从 IL_002b 处返回了一个 get_Task() ,这时候你就应该明白,为什么主线程不会被阻塞,因为人家返回的是 Task ,对吧,最后的 http 结果会藏在 Task 中,这样是不是就很好理解了。

    <3> Main
    Main方法没有做任何改变,原来是什么样现在还是什么样。

    三:将 IL 代码 回写为 C#

    1. 完整 C# 代码
      通过前面一部分你应该对 await ,async 在 IL 层面有了一个框架性的认识,这里我就全部反写成 C# 代码:

    class Program
    {
    static void Main(string[] args)
    {
    var html = GetResult();

        Console.WriteLine("稍等... 正在下载 cnblogs -> html \r\n");
    
        var content = html.Result;
    
        Console.WriteLine(content);
    }
    
    static Task<string> GetResult()
    {
        GetResult stateMachine = new GetResult();
    
        stateMachine.builder = AsyncTaskMethodBuilder<string>.Create();
    
        stateMachine.state = -1;
    
        stateMachine.builder.Start(ref stateMachine);
    
        return stateMachine.builder.Task;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    }

    class GetResult : IAsyncStateMachine
    {
    public int state;
    public AsyncTaskMethodBuilder builder;
    private WebClient client;
    private string content;
    private string s3;
    private TaskAwaiter awaiter;

    public void MoveNext()
    {
        var result = string.Empty;
        TaskAwaiter<string> localAwaiter;
        GetResult stateMachine;
    
        int num = state;
    
        try
        {
            if (num == 0)
            {
                localAwaiter = awaiter;
                awaiter = default(TaskAwaiter<string>);
                num = state = -1;
            }
            else
            {
                client = new WebClient();
    
                localAwaiter = client.DownloadStringTaskAsync(new Uri("http://cnblogs.com")).GetAwaiter();
    
                if (!localAwaiter.IsCompleted)
                {
                    num = state = 0;
                    awaiter = localAwaiter;
                    stateMachine = this;
                    builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref stateMachine);
                    return;
                }
            }
    
            s3 = localAwaiter.GetResult();
            content = s3;
            s3 = null;
            result = content;
        }
        catch (Exception exx)
        {
            state = -2;
            client = null;
            content = null;
            builder.SetException(exx);
        }
    
        state = -2;
        client = null;
        content = null;
        builder.SetResult(result);
    }
    
    public void SetStateMachine(IAsyncStateMachine stateMachine) { }
    
      23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    }
    可以看到,回写成 C# 代码之后跑起来是没有任何问题的,为了方便理解,我先来画一张流程图。
    在这里插入图片描述

    通过上面的 xmind,它基本流程就是: stateMachine.builder.Start(ref stateMachine) -> GetResult.MoveNext -> client.DownloadStringTaskAsync -> localAwaiter.IsCompleted = false -> builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref stateMachine) -> GetResult.MoveNext -> localAwaiter.GetResult() -> builder.SetResult(result)

    1. 剖析 AsyncTaskMethodBuilder
      其实你仔细观察会发现,所谓的 await,async 的异步化运作都是由 AsyncTaskMethodBuilder 承载的,如异步任务的启动,对html结果的封送,接触底层IO,其中 Task 对应着 AsyncTaskMethodBuilder, Task 对应着 AsyncTaskMethodBuilder, 这也是为什么编译器在 async 处一直提示你返回 Task 和 Task,如果不这样的话的就找不到对应 AsyncTaskMethodBuilder 了,对吧,如下图:

    在这里插入图片描述

    然后着重看下 AwaitUnsafeOnCompleted 方法,这个方法非常重要,其注释如下:

     //
        // Summary:
        //     Schedules the state machine to proceed to the next action when the specified
        //     awaiter completes. This method can be called from partially trusted code.
        public void AwaitUnsafeOnCompleted<[NullableAttribute(0)] TAwaiter, [NullableAttribute(0)] TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
            where TAwaiter : ICriticalNotifyCompletion
            where TStateMachine : IAsyncStateMachine;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    一旦调用了这个方法,就需要等待 底层IO 将任务处理完毕之后二次回调 GetResult.MoveNext,也就表示要么异常要么完成任务, Awaiter 包装的 Task 结果封送到 builder.SetResult。

    然后简单说一下 状态机 的走法,通过调试会发现这里会走 两次 MoveNext,一次启动,一次拿结果。

    <1> 第一次回调 MoveNext
    第一次 MoveNext 的触发由 stateMachine.builder.Start(ref stateMachine) 发起,可以用 dnspy 去调试一下,如下图:

    在这里插入图片描述

    <2> 第二次回调 MoveNext
    第二次 MoveNext 的触发由 builder.AwaitUnsafeOnCompleted(ref localAwaiter, ref stateMachine) 开始,可以看python基础教程

    到一旦 网络驱动程序 处理完毕后就由线程池IO线程主动发起到最后触发代码中的 MoveNext,最后就是到 awaiter 中获取 task 的 result 处结束,如下图:

    在这里插入图片描述

    四: 总结
    语法糖有简单和复杂之分,复杂的也不要怕,学会将 IL 代码翻译成 C# ,或许你以前很多不明白的地方此时都会豁然开朗,不是吗?

    相关技术文章

    最新源码

    下载排行榜

    点击QQ咨询
    开通会员
    返回顶部
    ×
    微信扫码支付
    微信扫码支付
    确定支付下载
    请使用微信描二维码支付
    ×

    提示信息

    ×

    选择支付方式

    • 微信支付
    • 支付宝付款
    确定支付下载