• 进阶篇:以IL为剑,直指async/await


    接上篇:30分钟?不需要,轻松读懂IL,这篇主要从IL入手来理解async/await的工作原理。

    先简单介绍下async/await,这是.net 4.5引入的语法糖,配合Task使用可以非常优雅的写异步操作代码,它本身并不会去创建一个新线程,线程的工作还是由Task来做,async/await只是让开发人员以直观的方式写异步操作代码,而不像以前那样到处都是callback或事件。

    async/await IL翻译

    先写个简单的例子:

    复制代码
     1 using System;
     2 using System.Threading.Tasks;
     3 
     4 namespace ILLearn
     5 {
     6     class Program
     7     {
     8         static void Main(string[] args)
     9         {
    10             DisplayDataAsync();
    11 
    12             Console.ReadLine();
    13         }
    14 
    15         static async void DisplayDataAsync()
    16         {
    17             Console.WriteLine("start");
    18 
    19             var data = await GetData();
    20 
    21             Console.WriteLine(data);
    22 
    23             Console.WriteLine("end");
    24         }
    25 
    26         static async Task<string> GetData()
    27         {
    28             await Task.Run(async () => await Task.Delay(1000));
    29             return "data";
    30         }
    31     }
    32 }
    复制代码

    编译: csc /debug- /optimize+ /out:program.exe program.cs 生成program.exe文件,用ildasm.exe打开,如下:

    发现多出来两个结构,带<>符号的一般都是编译时生成的:<DisplayDataAsync>d_1和<GetData>d_2,

    <DisplayDataAsync>d_1是我们这次的目标,来分析一下:

    这个结构是给DisplayDataAsync用的,名字不好,实现了IAsyncStateMachine接口,看名字知道一个状态机接口,原来是编译时生成了一个状态机,有3个字段,2个接口函数,我们整理一下状态机代码:

    复制代码
     1 struct GetDataAsyncStateMachine : IAsyncStateMachine
     2 {
     3     public int State;
     4 
     5     public AsyncVoidMethodBuilder Builder;
     6 
     7     private TaskAwaiter<string> _taskAwaiter;
     8 
     9     void IAsyncStateMachine.MoveNext();
    10 
    11     void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine);
    12 }
    复制代码

    这样就好看多了。

    再来看看我们写的DisplayDataAsync的IL:

    双击

    复制代码
     1 .method private hidebysig static void  DisplayDataAsync() cil managed
     2 {
     3   .custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = ( 01 00 26 49 4C 4C 65 61 72 6E 2E 50 72 6F 67 72   // ..&ILLearn.Progr
     4                                                                                                                                      61 6D 2B 3C 44 69 73 70 6C 61 79 44 61 74 61 41   // am+<DisplayDataA
     5                                                                                                                                      73 79 6E 63 3E 64 5F 5F 31 00 00 )                // sync>d__1..
     6   // 代码大小       37 (0x25)
     7   .maxstack  2
     8   .locals init (valuetype ILLearn.Program/'<DisplayDataAsync>d__1' V_0,  //这里还是局部变量,第1个是valuetype也就是值类型<DisplayDataAsync>d__1,在上面知道这是一个状态机 DisplayDataAsyncStateMachine
     9            valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder V_1) //第2个局部变量也是值类型,叫AsyncVoidMethodBuilder,在System.Runtime.CompilerServices命名空间下
    10   IL_0000:  ldloca.s   V_0  //加载第1个局部变量的地址,因为是结构,在栈上,通过地址来调用函数
    11   IL_0002:  call       valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()  //调用AsyncVoidMethodBuilder的create函数,用的是call,并且没有实例,所以create()是个静态函数
    12   IL_0007:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder ILLearn.Program/'<DisplayDataAsync>d__1'::'<>t__builder'  //把create()的结果存到DisplayDataAsyncStateMachine结构的Builder字段
    13   IL_000c:  ldloca.s   V_0  //加载第1个局部变量的地址,还是为了给这个结构的变量赋值
    14   IL_000e:  ldc.i4.m1  //加载整数 -1,上篇没有说,这个m表示minus,也就是负号
    15   IL_000f:  stfld      int32 ILLearn.Program/'<DisplayDataAsync>d__1'::'<>1__state'  //把-1存到DisplayDataAsyncStateMachine的State字段
    16   IL_0014:  ldloc.0   //加载第1个局部变量
    17   IL_0015:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder ILLearn.Program/'<DisplayDataAsync>d__1'::'<>t__builder' //获取第1个局部变量的Builder字段,也就是上面create()出来的
    18   IL_001a:  stloc.1  //存到第2个局部变量中 V_1 = DisplayDataAsyncStateMachine.Builder
    19   IL_001b:  ldloca.s   V_1  //加载第1个局部变量地址
    20   IL_001d:  ldloca.s   V_0  //加载第2个局部变量地址
    21   IL_001f:  call       instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Start<valuetype ILLearn.Program/'<DisplayDataAsync>d__1'>(!!0&)  //调用V_0的start方法,方法有个参数!!0&,这看上去有点奇怪,指的是上面加载的V_1的地址
    22   IL_0024:  ret //返回
    23 } // end of method Program::DisplayDataAsync
    复制代码

    好了,这个函数的意思差不多搞懂了,我们先把它翻译成容易看懂的C#代码,大概是这个样子:

    复制代码
     1 public void DisplayDataAsync()
     2 {
     3     DisplayDataAsyncStateMachine stateMachine;
     4 
     5     stateMachine.Builder = AsyncVoidMethodBuilder.Create();
     6 
     7     stateMachine.State = -1;
     8 
     9     AsyncVoidMethodBuilder builder = stateMachine.Builder;
    10 
    11     builder.Start(ref stateMachine);
    12 }
    复制代码

    与源代码完全不一样。

    GetDataAsyncStateMachine还有两个接口函数的IL需要看下,接下来先看看这两个函数SetStateMachine和MoveNext的IL代码,把它也翻译过来,注意:IL里用的<DisplayDataAsync>d_1,<>1_state,<>_builder,<>u_1都可以用GetDataAsyncStateMachine,State, Builder,_taskAwaiter来表示了,这样更容易理解一些。

    MoveNext:

    复制代码
      1 .method private hidebysig newslot virtual final 
      2         instance void  MoveNext() cil managed
      3 {
      4   .override [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext
      5   // 代码大小       175 (0xaf)
      6   .maxstack  3
      7   .locals init (int32 V_0,  
      8            valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string> V_1,
      9            class [mscorlib]System.Exception V_2)  //3个局部变量
     10   IL_0000:  ldarg.0  //加载第0个参数,也就是本身
     11   IL_0001:  ldfld      int32 ILLearn.Program/'<DisplayDataAsync>d__1'::'<>1__state'  //加载字段State
     12   IL_0006:  stloc.0  //存到第1个局部变量中,也就是V_0 = State
     13   .try //try 块
     14   {
     15     IL_0007:  ldloc.0  //加载第1个局部变量
     16     IL_0008:  brfalse.s  IL_0048  //是false也就是 V_0 == 0则跳转到IL_0048
     17     IL_000a:  ldstr      "start"  //加载string "start"
     18     IL_000f:  call       void [mscorlib]System.Console::WriteLine(string)  //调用Console.WriteLine("start")
     19     IL_0014:  call       class [mscorlib]System.Threading.Tasks.Task`1<string> ILLearn.Program::GetData()  //调用静态方法Program.GetData()
     20     IL_0019:  callvirt   instance valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<!0> class [mscorlib]System.Threading.Tasks.Task`1<string>::GetAwaiter() //调用GetData()返回Task的GetAwaiter()方法
     21     IL_001e:  stloc.1  //把GetAwaiter()的结果存到第2个局部变量中也就是V_1 = GetData().GetAwaiter()
     22     IL_001f:  ldloca.s   V_1  //加载第2个局部变量V_1的地址
     23     IL_0021:  call       instance bool valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string>::get_IsCompleted()  //调用实例属性 IsCompleted
     24     IL_0026:  brtrue.s   IL_0064  //如果V_1.IsCompleted == true则跳转到IL_0064
     25     IL_0028:  ldarg.0  //加载this
     26     IL_0029:  ldc.i4.0  //加载整数0
     27     IL_002a:  dup  //复制, 因为要存两份
     28     IL_002b:  stloc.0  //存到第1个局部变量中,V_0=0
     29     IL_002c:  stfld      int32 ILLearn.Program/'<DisplayDataAsync>d__1'::'<>1__state' //存到State,State=0
     30     IL_0031:  ldarg.0  //加载this
     31     IL_0032:  ldloc.1  //加载第2个局部变量
     32     IL_0033:  stfld      valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string> ILLearn.Program/'<DisplayDataAsync>d__1'::'<>u__1'  //存到<>u__1也就是_taskAwaiter中,_taskAwaiter = V_1
     33     IL_0038:  ldarg.0  //加载this
     34     IL_0039:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder ILLearn.Program/'<DisplayDataAsync>d__1'::'<>t__builder' //加载Builder的地址
     35     IL_003e:  ldloca.s   V_1  //加载V_1的地址
     36     IL_0040:  ldarg.0  //加载this
     37     IL_0041:  call       instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::AwaitUnsafeOnCompleted<valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string>,valuetype ILLearn.Program/'<DisplayDataAsync>d__1'>(!!0&,!!1&)//调用Builder的AwaitUnsafeOnCompleted函数,第1个参数是v1的地址,第2个是this,都是引用
     38     IL_0046:  leave.s    IL_00ae  // 跳到IL_00ae,也就是return
     39     IL_0048:  ldarg.0  //从IL_0008跳过来,加载this
     40     IL_0049:  ldfld      valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string> ILLearn.Program/'<DisplayDataAsync>d__1'::'<>u__1'  //加载_taskAwaiter
     41     IL_004e:  stloc.1  //存到第2个局部变量,V_1 = _taskAwaiter
     42     IL_004f:  ldarg.0  //加载this
     43     IL_0050:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string> ILLearn.Program/'<DisplayDataAsync>d__1'::'<>u__1'  //加载_taskAwaiter地址
     44     IL_0055:  initobj    valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string>  //初始化结构,也就是_taskAwaiter = default(TaskAwaiter<string>)
     45     IL_005b:  ldarg.0  //加载this
     46     IL_005c:  ldc.i4.m1  //加载-1
     47     IL_005d:  dup  //复制
     48     IL_005e:  stloc.0  //把-1存到V_0中,V_0 = -1
     49     IL_005f:  stfld      int32 ILLearn.Program/'<DisplayDataAsync>d__1'::'<>1__state'  //存到State,State=-1
     50     IL_0064:  ldloca.s   V_1  //从IL_0026跳过来的,加载V_1的地址
     51     IL_0066:  call       instance !0 valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string>::GetResult()  //调用V_1.GetResult()
     52     IL_006b:  ldloca.s   V_1 //加载V_1的地址
     53     IL_006d:  initobj    valuetype [mscorlib]System.Runtime.CompilerServices.TaskAwaiter`1<string>  //初始化结构,也就是V_1 = default(TaskAwaiter<string>)
     54     IL_0073:  call       void [mscorlib]System.Console::WriteLine(string)  // Console.WriteLine 写GetResult返回的值
     55     IL_0078:  ldstr      "end"
     56     IL_007d:  call       void [mscorlib]System.Console::WriteLine(string)  //Console.WriteLine("end")
     57     IL_0082:  leave.s    IL_009b  //没异常,跳到IL_009b
     58   }  // end .try
     59   catch [mscorlib]System.Exception  //catch 块
     60   {
     61     IL_0084:  stloc.2  //把异常存到V_2
     62     IL_0085:  ldarg.0  //加载this
     63     IL_0086:  ldc.i4.s   -2  //加载-2
     64     IL_0088:  stfld      int32 ILLearn.Program/'<DisplayDataAsync>d__1'::'<>1__state'  //State = -2
     65     IL_008d:  ldarg.0  //加载this
     66     IL_008e:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder ILLearn.Program/'<DisplayDataAsync>d__1'::'<>t__builder'  //加载Builder的地址
     67     IL_0093:  ldloc.2  //加载第3个局部变量Exception
     68     IL_0094:  call       instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetException(class [mscorlib]System.Exception)  //调用Builder.SetException,参数就是第3个局部变量
     69     IL_0099:  leave.s    IL_00ae  //return
     70   }  // end handler
     71   IL_009b:  ldarg.0  //加载this
     72   IL_009c:  ldc.i4.s   -2 //加载-2
     73   IL_009e:  stfld      int32 ILLearn.Program/'<DisplayDataAsync>d__1'::'<>1__state'  //State = -2
     74   IL_00a3:  ldarg.0 //加载this
     75   IL_00a4:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder ILLearn.Program/'<DisplayDataAsync>d__1'::'<>t__builder'//加载Builder的地址
     76   IL_00a9:  call       instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetResult()  //Builder.SetResult()
     77   IL_00ae:  ret  //return
     78 } // end of method '<DisplayDataAsync>d__1'::MoveNext
     79 
     80 翻译整理一下:
     81 V_0用state表示, V_1用awaiter表示,V_2用ex表示
     82 
     83 void IAsyncStateMachine.MoveNext()
     84 {
     85     int state = State;
     86     try
     87     {
     88         TaskAwaiter<string> awaiter;
     89         if (state != 0)  // 状态不是0就进来,默认是-1
     90         {
     91             Console.WriteLine("start");  //  执行 await 之前的部分
     92 
     93             awaiter = Program.GetData().GetAwaiter();  // 获取 awaiter
     94 
     95             if (!awaiter.IsCompleted)  //判断是否完成,完成的话就不用分开了,直接执行后面的
     96             {
     97                 state = 0;
     98                 State = 0;  // 把状态变为0, awaiter执行完成后就不用进这里了
     99                 _taskAwaiter = awaiter;  // 保存awaiter, awaiter回来后要靠_taskAwaiter来取结果
    100                 Builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);  // 这里面主要是构造一个action - MoveNextRunner,用来在awaiter.complete事件触发后走到这个状态机的MoveNext(),上面把state变了0了,再走这个函数的话就可以走到await后面的部分,后面再详细讲
    101                 return;  // 返回
    102             }
    103         }
    104         else
    105         {
    106             awaiter = _taskAwaiter;
    107             state = -1;
    108             State = -1;
    109         }
    110 
    111         var result = awaiter.GetResult(); //awaiter回来后取得结果
    112 
    113         Console.WriteLine(result);  // 走 await 后面的部分
    114 
    115         Console.WriteLine("end");
    116     }
    117     catch(Exception ex)
    118     {
    119         State = -2;
    120         Builder.SetException(ex);
    121     }
    122     
    123     State = -2;
    124     Builder.SetResult();
    125 }
    复制代码

    SetStateMachine:

    复制代码
     1 .method private hidebysig newslot virtual final 
     2         instance void  SetStateMachine(class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine stateMachine) cil managed
     3 {
     4   .custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor() = ( 01 00 00 00 ) 
     5   .override [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine::SetStateMachine
     6   // 代码大小       13 (0xd)
     7   .maxstack  8
     8   IL_0000:  ldarg.0
     9   IL_0001:  ldflda     valuetype [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder ILLearn.Program/'<DisplayDataAsync>d__1'::'<>t__builder'
    10   IL_0006:  ldarg.1
    11   IL_0007:  call       instance void [mscorlib]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetStateMachine(class [mscorlib]System.Runtime.CompilerServices.IAsyncStateMachine)
    12   IL_000c:  ret
    13 } // end of method '<DisplayDataAsync>d__1'::SetStateMachine
    14 
    15 这个很简单,就不一一写了,直接翻译:
    16 void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    17 {
    18     Builder.SetStateMachine(stateMachine);
    19 }
    复制代码

    因为是照着IL直译,代码可能有点冗余,不过不伤大雅。

    async/await原理

    现在疏理一下,从DisplayDataAsync开始,先是创建一个状态机,把状态变量State初始化为-1,Builder使用AsyncVoidMethodBuilder.Create来创建,既而调用这个builder的Start函数并把状态机的引用传过去。

    那重点就是这个AsyncVoidMethodBuilder的作用,AsyncVoidMethodBuilder在命名空间System.Runtime.CompilerServices下,我们来读一下它的源码,.net的BCL已经开源了,所以直接去github上找就行了。

    https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Runtime/CompilerServices/AsyncMethodBuilder.cs

    这文件里面有这么几个重要类AsyncVoidMethodBuilder,AsyncTaskMethodBuilder,AsyncTaskMethodBuilder<T>,AsyncMethodBuilderCore及AsyncMethodBuilderCore内的MoveNextRunner。

    首先为什么DsiplayDataAsync用到的是AsyncVoidMethodBuilder,因为DisplayDataAsync返回的是void,在ildasm里双击GetData你会发现如下IL:

    1 IL_0002: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<string>::Create()

    GetData用的是AsyncTaskMethodBuilder<string>,因为GetData返回的是Task<string>。那我们就知道了,AsyncVoidMethodBuilder,AsyncTaskMethodBuilder,AsyncTaskMethodBuilder<T>这三个类分别对应返回为void, Task和Task<T>的异步函数,因为async标记的函数只能返回这三种类型。这三个类的功能差不多,代码大同小异,我们就拿用到的AsyncVoidMethodBuilder来说。

    先看最先调用的Create()函数:

    复制代码
    1 public static AsyncVoidMethodBuilder Create()
    2 {
    3     SynchronizationContext sc = SynchronizationContext.CurrentNoFlow;
    4     if (sc != null)
    5         sc.OperationStarted();
    6     return new AsyncVoidMethodBuilder() { m_synchronizationContext = sc };
    7 }
    复制代码

    SynchronizationContext.CurrentNoFlow作用是取得当前线程的SynchronizationContext,这个有什么用呢,SynchronizationContext可以算是一个抽象概念的类(这个类本身不是抽象的),它提供了线程间通讯的桥梁,一般线程的SynchronizationContext.Current为空,但主线程除外,比如对于WinForm,在第一个窗体创建时,系统会给主线程添加SynchronizationContext,也就是SynchronizationContext.Current = new WinFormSynchronizationContext(),WinFormSynchronizationContext是继承SynchronizationContext并重新实现了一些方法如Send,Post,Send, Post都是通过Control.Invoke/BeginInvoke来实现与UI线程的通讯。

    对应的WPF的就是DispatcherSynchronizationContext,Asp.net就是AspNetSynchronizationContext。

    当然,这里的SynchronizationContext是用来做跨线程Exception处理的,Task的Exception为什么能在外面捕获到,就靠这个SynchronizationContext,这个后面详细再讲。

    好了,Create函数看完,接下来看Start()函数。

    复制代码
     1 public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
     2 {
     3     if (stateMachine == null) throw new ArgumentNullException("stateMachine");
     4     Contract.EndContractBlock();
     5 
     6     ExecutionContextSwitcher ecs = default(ExecutionContextSwitcher);
     7     RuntimeHelpers.PrepareConstrainedRegions();
     8     try
     9     {
    10         ExecutionContext.EstablishCopyOnWriteScope(ref ecs);
    11         stateMachine.MoveNext();
    12     }
    13     finally
    14     {
    15         ecs.Undo();
    16     }
    17 }
    复制代码
    Contract.EndContractBlock();这个是一个契约标记,一般用在throw后面,没功能性的作用,这里不多讲,有兴趣的可以去翻下契约式编程。

    先看看ExecutionContext

    https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Threading/ExecutionContext.cs

    ExecutionContext可以认为是一个容器,里面包含了一组context,SynchronizationContext是里面其中一个,还有如SecretContext,LogicContext等,代表了线程所执行的上下文。

    ExecutionContextSwitcher这个类型又是干什么的呢,看代码:

    复制代码
     1 internal struct ExecutionContextSwitcher
     2 {
     3     internal ExecutionContext m_ec;
     4     internal SynchronizationContext m_sc;
     5 
     6     internal void Undo()
     7     {
     8         SynchronizationContext.SetSynchronizationContext(m_sc);
     9         ExecutionContext.Restore(m_ec);
    10     }
    11 }
    复制代码

    也是一个结构,主要用来做Undo操作的,也就是在执行MoveNext时如果出现异常,可以恢复原来的上下文。

    接着看Start函数,RuntimeHelpers.PrepareConstrainedRegions() 就是CER(Constrained Execution Region),一般由RuntimeHelpers.PrepareConstrainedRegions() + try..catch..finally组成,用来告诉CLR这段代码很重要,不管是什么异常都不要打断,为了保证不被打断, CER内(catch和finally块)的代码不能在堆上有操作,并且预先编译好CER内的代码,一切都是为了防止被打断。

    说到预编译,CLR里还有个操作也是要预编译的,就是派生自CriticalFinalizerObjectFinalizer的类,这些类会确保它们的Finalize会被执行。

    GC如果是因为内存不足而触发,而这时Finalize如果没有预编译,就有可能发生没有内存可供Finalize编译,Finalize得不到执行,对象也不能被释放,从而造成资源泄漏。

    进入try块,执行ExecutionContext.EstblishCopyOnWriteScope(ref ecs)这个函数,接着看它的代码:

    1 static internal void EstablishCopyOnWriteScope(ref ExecutionContextSwitcher ecsw)
    2 {
    3     ecsw.m_ec = Capture();
    4     ecsw.m_sc = SynchronizationContext.CurrentNoFlow;
    5 }

    原来是给ExecutionContextSwitcher的属性赋值,Capture函数是抓取当前线程的ExecutionContext,这样ExecutionContextSwitcher里的Context就可以保存下来以便异常时恢复了。

    继续Start函数,最重要的stateMachine.MoveNext()来了,上面一大堆都是为了这个家伙的安全执行。

    整个Start看完,目的也就是执行MoveNext,那我们看看状态机里MoveNext干了些什么:

    看看我们上面翻译的结果:

    复制代码
     1 void IAsyncStateMachine.MoveNext()
     2 {
     3     int state = State;
     4 
     5     try
     6     {
     7         TaskAwaiter<string> awaiter;
     8 
     9         if (state != 0) // 状态不是0就进来,默认是-1
    10         {
    11             Console.WriteLine("start"); // 执行 await 之前的部分
    12             awaiter = Program.GetData().GetAwaiter(); // 获取 awaiter
    13 
    14             if (!awaiter.IsCompleted) //判断是否完成,完成的话就不用分开了,直接执行后面的
    15             {
    16                 state = 0;
    17                 State = 0; // 把状态变为0, awaiter执行完成后再次MoveNext就不用进这里了
    18                 _taskAwaiter = awaiter; // 保存awaiter, awaiter回来后要靠_taskAwaiter来取结果
    19                 Builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); // 这里面主要是构造一个action - MoveNextRunner,用来在awaiter.complete事件触发后继续走这个状态机的MoveNext(),上面把state变了0了,再走这个函数的话就可以走到await后面的部分,下面再详细讲
    20 
    21                 return; // 返回
    22             }
    23         }
    24         else
    25         {
    26             awaiter = _taskAwaiter;
    27             state = -1;
    28             State = -1;
    29         }
    30 
    31         var result = awaiter.GetResult(); //awaiter回来后取得结果
    32         Console.WriteLine(result); // 走 await 后面的部分
    33         Console.WriteLine("end");
    34     }
    35     catch (Exception ex)
    36     {
    37         State = -2;
    38         Builder.SetException(ex);
    39     }
    40 
    41     State = -2;
    42     Builder.SetResult();
    43 }
    复制代码

    可以把原始代码看成三段,如图:

    第一次进来由于state是-1,所以先执行第一段,接着是第二段,把state置为0并且拿到awaiter做Builder.AwaitUnsafeOnCompleted(ref awaiter, ref this)操作,这个操作里面会在取到数据后再次MoveNext,因为state为0,所以就走到第三段,整个过程是这样。

    我们详细看看Builder.AwaitUnsafeOnCompleted这个操作是怎么调用第二次MoveNext的。

    复制代码
     1 public void AwaitOnCompleted<TAwaiter, TStateMachine>(
     2     ref TAwaiter awaiter, ref TStateMachine stateMachine)
     3     where TAwaiter : INotifyCompletion
     4     where TStateMachine : IAsyncStateMachine
     5 {
     6     try
     7     {
     8         AsyncMethodBuilderCore.MoveNextRunner runnerToInitialize = null;
     9         var continuation = m_coreState.GetCompletionAction(AsyncCausalityTracer.LoggingOn ? this.Task : null, ref runnerToInitialize);
    10         Contract.Assert(continuation != null, "GetCompletionAction should always return a valid action.");
    11 
    12         // If this is our first await, such that we've not yet boxed the state machine, do so now.
    13         if (m_coreState.m_stateMachine == null)
    14         {
    15             if (AsyncCausalityTracer.LoggingOn)
    16                 AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, this.Task.Id, "Async: " + stateMachine.GetType().Name, 0);
    17                     
    18             m_coreState.PostBoxInitialization(stateMachine, runnerToInitialize, null);
    19         }
    20 
    21         awaiter.OnCompleted(continuation);
    22     }
    23     catch (Exception exc)
    24     {
    25         AsyncMethodBuilderCore.ThrowAsync(exc, targetContext: null);
    26     }
    27 }
    复制代码

    一点一点看,先调用了m_coreState.GetCompletionAction,m_coreState是AsyncMethodBuilderCore类型,来看看它的实现:

    复制代码
     1 internal Action GetCompletionAction(Task taskForTracing, ref MoveNextRunner runnerToInitialize)
     2 {
     3     Contract.Assert(m_defaultContextAction == null || m_stateMachine != null,
     4         "Expected non-null m_stateMachine on non-null m_defaultContextAction");
     5 
     6     Debugger.NotifyOfCrossThreadDependency();
     7 
     8     var capturedContext = ExecutionContext.FastCapture();  //获取当前线程的ExecutionContext
     9     Action action;
    10     MoveNextRunner runner;
    11     if (capturedContext != null && capturedContext.IsPreAllocatedDefault)
    12     {
    13         action = m_defaultContextAction;
    14         if (action != null)
    15         {
    16             Contract.Assert(m_stateMachine != null, "If the delegate was set, the state machine should have been as well.");
    17             return action;
    18         }
    19         runner = new MoveNextRunner(capturedContext, m_stateMachine);  //new一个MoveNextRunner实例,并把ExecutionContext和状态机传过去
    20 
    21         action = new Action(runner.Run);  //runner.Run的action
    22         if (taskForTracing != null)
    23         {
    24             m_defaultContextAction = action = OutputAsyncCausalityEvents(taskForTracing, action);
    25         }
    26         else
    27         {
    28             m_defaultContextAction = action;
    29         }
    30     }
    31     else
    32     {
    33         runner = new MoveNextRunner(capturedContext, m_stateMachine);
    34         action = new Action(runner.Run);
    35 
    36         if (taskForTracing != null)
    37         {
    38             action = OutputAsyncCausalityEvents(taskForTracing, action);
    39         }
    40     }
    41 
    42     if (m_stateMachine == null)
    43         runnerToInitialize = runner;
    44 
    45     return action;
    46 }
    复制代码

    这段代码看起来比较简单,主要是针对MoveNextRunner实例,传递上下文和状态机给它,大家应该可以猜到MoveNext就是用这个MoveNextRunner.Run去实现了,这个函数返回的就是MoveNextRunner.Run。

    再回头看上面的代码,如果m_coreState.m_stateMachine == null,也就是第一次进来就先做PostBoxInitialization操作,看看PostBoxInitialization:

    复制代码
     1 internal void PostBoxInitialization(IAsyncStateMachine stateMachine, MoveNextRunner runner, Task builtTask)
     2 {
     3     if (builtTask != null)
     4     {
     5         if (AsyncCausalityTracer.LoggingOn)
     6             AsyncCausalityTracer.TraceOperationCreation(CausalityTraceLevel.Required, builtTask.Id, "Async: " + stateMachine.GetType().Name, 0);
     7 
     8         if (System.Threading.Tasks.Task.s_asyncDebuggingEnabled)
     9             System.Threading.Tasks.Task.AddToActiveTasks(builtTask);
    10     }
    11 
    12     m_stateMachine = stateMachine;  //给m_stateMachine赋值,因为m_stateMachine是internal IAsyncStateMachine m_stateMachine;这样定义的,所以把struct stateMachine传给这个接口类型时会装箱,目的是在Builder里面保存这个状态机,下次不会走这了
    13     m_stateMachine.SetStateMachine(m_stateMachine);
    14 
    15     Contract.Assert(runner.m_stateMachine == null, "The runner's state machine should not yet have been populated.");
    16     Contract.Assert(m_stateMachine != null, "The builder's state machine field should have been initialized.");
    17 
    18     runner.m_stateMachine = m_stateMachine;
    19 }
    复制代码

    这个函数的目的有两个,一个是给状态机装箱保存下来,另一个是给runner的状态机赋值。

    再看回上面的AwaitUnsafeOnCompleted函数,到awaiter.UnsafeOnCompleted(continuation)了,这个算是核心,主要就是等这个回来再调用continuation,continuation我们知道是MoveNextRunner的Run函数,先看看这个Run函数:

    复制代码
     1 internal void Run()
     2 {
     3     Contract.Assert(m_stateMachine != null, "The state machine must have been set before calling Run.");
     4 
     5     if (m_context != null)
     6     {
     7         try
     8         {
     9             ContextCallback callback = s_invokeMoveNext;
    10             if (callback == null) { s_invokeMoveNext = callback = InvokeMoveNext; }
    11 
    12             ExecutionContext.Run(m_context, callback, m_stateMachine, preserveSyncCtx: true);  //主要就是用ExecutionContext应用到当前线程来执行这个((IAsyncStateMachine)stateMachine).MoveNext()
    13         }
    14         finally { m_context.Dispose(); }
    15     }
    16     else
    17     {
    18         m_stateMachine.MoveNext();
    19     }
    20 }
    21 
    22 private static ContextCallback s_invokeMoveNext;
    23 
    24 private static void InvokeMoveNext(object stateMachine)
    25 {
    26     ((IAsyncStateMachine)stateMachine).MoveNext();
    27 }
    复制代码

    Run的目的很简单,m_context是await之前的线程上下文,所以就是以执行Console.WriteLine("start")一样的线程上下文去执行MoveNext,用这个ExecutionContext.Run并不是说Console.WriteLine("start")和Console.WriteLine("end")会在同一个线程,ExecutionContext.Run只是在线程池里拿一个空闲的线程,赋予同样的上下文来执行MoveNext()。

    现在只有awaiter.UnsafeOnCompleted(continuation)还没讲,不过功能已经清楚,就是awaiter completed后回调continuation,追根到底看看它是怎么实现的:

    https://github.com/dotnet/coreclr/blob/master/src/mscorlib/src/System/Runtime/CompilerServices/TaskAwaiter.cs
    1 public void UnsafeOnCompleted(Action continuation)
    2 {
    3     OnCompletedInternal(m_task, continuation, continueOnCapturedContext: true, flowExecutionContext: false);
    4 }

    continueOnCapturedContext这个是由Task.ConfigureAwait(continueOnCapturedContext)来控制的,true则表示执行完task后转到SynchronizationContext所在的线程上去执行await后面的部分,比如说更新UI就必须在UI线程上,这个就需要设为true,如果不是要更新UI,而是还有很多的数据需要本地计算,则最好设为false,这时会在task执行完成后在线程池中拿出一个空闲的工作线程来做await后面的事,当然在Asp.net里要注意HttpContext.Current可能在false时会为Null,操作时需要注意。接着看OnCompletedInternal的代码:

    复制代码
     1 internal static void OnCompletedInternal(Task task, Action continuation, bool continueOnCapturedContext, bool flowExecutionContext)
     2 {
     3     if (continuation == null) throw new ArgumentNullException("continuation");
     4     StackCrawlMark stackMark = StackCrawlMark.LookForMyCaller;
     5 
     6     if (TplEtwProvider.Log.IsEnabled() || Task.s_asyncDebuggingEnabled)
     7     {
     8         continuation = OutputWaitEtwEvents(task, continuation);
     9     }
    10 
    11     task.SetContinuationForAwait(continuation, continueOnCapturedContext, flowExecutionContext, ref stackMark);
    12 }
    复制代码

    主要是调用SetContinuationForAwait:

    复制代码
     1 internal void SetContinuationForAwait(
     2             Action continuationAction, bool continueOnCapturedContext, bool flowExecutionContext, ref StackCrawlMark stackMark)
     3 {
     4     Contract.Requires(continuationAction != null);
     5 
     6 
     7     TaskContinuation tc = null;
     8 
     9     if (continueOnCapturedContext)  //如果需要用到SynchronizationContext
    10     {
    11         var syncCtx = SynchronizationContext.CurrentNoFlow;  //获取当前SynchronizationContext
    12         if (syncCtx != null && syncCtx.GetType() != typeof(SynchronizationContext))  //当前SynchronizationContext和传进来的SynchronizationContext不相等
    13         {
    14             tc = new SynchronizationContextAwaitTaskContinuation(syncCtx, continuationAction, flowExecutionContext, ref stackMark);  //用SynchronizationContext来转到目标线程去执行
    15         }
    16         Else
    17                 {
    18             var scheduler = TaskScheduler.InternalCurrent;
    19             if (scheduler != null && scheduler != TaskScheduler.Default)
    20             {
    21                 tc = new TaskSchedulerAwaitTaskContinuation(scheduler, continuationAction, flowExecutionContext, ref stackMark);
    22             }
    23         }
    24     }
    25 
    26     if (tc == null && flowExecutionContext)
    27     {
    28         tc = new AwaitTaskContinuation(continuationAction, flowExecutionContext: true, stackMark: ref stackMark); // continueOnCapturedContext = false时
    29     }
    30 
    31     if (tc != null)
    32     {
    33         if (!AddTaskContinuation(tc, addBeforeOthers: false))
    34             tc.Run(this, bCanInlineContinuationTask: false);  //开始执行Run
    35     }
    36     else
    37     {
    38         Contract.Assert(!flowExecutionContext, "We already determined we're not required to flow context.");
    39         if (!AddTaskContinuation(continuationAction, addBeforeOthers: false))
    40             AwaitTaskContinuation.UnsafeScheduleAction(continuationAction, this);
    41     }
    42 }
    复制代码

    最主要看是怎么Run的,先看第一种,continueOnCapturedContext为true的:

    复制代码
     1 internal sealed override void Run(Task task, bool canInlineContinuationTask)
     2 {
     3     if (canInlineContinuationTask && this.m_syncContext == SynchronizationContext.CurrentNoFlow)  //如果当前线程的SynchronizationContext和syncContext一样,那表示就是一个线程,直接执行就好了
     4     {
     5         base.RunCallback(AwaitTaskContinuation.GetInvokeActionCallback(), this.m_action, ref Task.t_currentTask);
     6         return;
     7     }
     8     TplEtwProvider log = TplEtwProvider.Log;
     9     if (log.IsEnabled())
    10     {
    11         this.m_continuationId = Task.NewId();
    12         log.AwaitTaskContinuationScheduled((task.ExecutingTaskScheduler ?? TaskScheduler.Default).Id, task.Id, this.m_continuationId);
    13     }
    14     base.RunCallback(SynchronizationContextAwaitTaskContinuation.GetPostActionCallback(), this, ref Task.t_currentTask);  // 这里用到了GetPostActionCallback()来执行
    15 }
    复制代码

    看看PostAction:

    复制代码
     1 private static void PostAction(object state)
     2 {
     3     SynchronizationContextAwaitTaskContinuation synchronizationContextAwaitTaskContinuation = (SynchronizationContextAwaitTaskContinuation)state;
     4     if (TplEtwProvider.Log.TasksSetActivityIds && synchronizationContextAwaitTaskContinuation.m_continuationId != 0)
     5     {
     6         synchronizationContextAwaitTaskContinuation.m_syncContext.Post(SynchronizationContextAwaitTaskContinuation.s_postCallback, SynchronizationContextAwaitTaskContinuation.GetActionLogDelegate(synchronizationContextAwaitTaskContinuation.m_continuationId, synchronizationContextAwaitTaskContinuation.m_action));  //看到了吧,用的是SynchronizationContext的Post来执行await后面的,如果SynchronizationContext是UI线程上的,那在Winform里就是control.BeginInvoke,在WPF里就是Dispatcher.BeginInvoke,转到UI线程执行
     7         return;
     8     }
     9     synchronizationContextAwaitTaskContinuation.m_syncContext.Post(SynchronizationContextAwaitTaskContinuation.s_postCallback, synchronizationContextAwaitTaskContinuation.m_action);
    10 }
    复制代码

    来看看第二种:continueOnCapturedContext为false:

    复制代码
     1 internal override void Run(Task task, bool canInlineContinuationTask)
     2 {
     3     if (canInlineContinuationTask && AwaitTaskContinuation.IsValidLocationForInlining)
     4     {
     5         this.RunCallback(AwaitTaskContinuation.GetInvokeActionCallback(), this.m_action, ref Task.t_currentTask);  //这里去到RunCallback
     6         return;
     7     }
     8     TplEtwProvider log = TplEtwProvider.Log;
     9     if (log.IsEnabled())
    10     {
    11         this.m_continuationId = Task.NewId();
    12         log.AwaitTaskContinuationScheduled((task.ExecutingTaskScheduler ?? TaskScheduler.Default).Id, task.Id, this.m_continuationId);
    13     }
    14     ThreadPool.UnsafeQueueCustomWorkItem(this, false); // 这也是通过线程池去运行
    15 }
    16 
    17 protected void RunCallback(ContextCallback callback, object state, ref Task currentTask)
    18 {
    19     Task task = currentTask;
    20     try
    21     {
    22         if (task != null)
    23         {
    24             currentTask = null;
    25         }
    26         if (this.m_capturedContext == null)
    27         {
    28             callback(state);
    29         }
    30         else
    31         {
    32             ExecutionContext.Run(this.m_capturedContext, callback, state, true); //就是通过ExecutionContext.Run去运行
    33         }
    34     }
    35     catch (Exception arg_2A_0)
    36     {
    37         AwaitTaskContinuation.ThrowAsyncIfNecessary(arg_2A_0);
    38     }
    39     finally
    40     {
    41         if (task != null)
    42         {
    43             currentTask = task;
    44         }
    45         if (this.m_capturedContext != null)
    46         {
    47             this.m_capturedContext.Dispose();
    48         }
    49     }
    50 }
    复制代码

    所以为false时就没SynchronizationContext什么事,线程池里拿个空闲线程出来运行就好了。上面有很大篇幅讲了awaiter.AwaitUnsafeOnCompleted的运行原理,因为async/await是配合awaitable用的,所以就一起分析。

    那现在这个简单的async/await例子就分析完了,可能有人会觉得状态机貌似没什么用,用if/else也能轻松做到这个,没必要用MoveNext。那是因为这里只有一个await,如果更多呢,if/else就很难控制,MoveNext就只需要关注状态变化就好了。写个有三个await的函数来看看:

    复制代码
     1 static async void DisplayDataAsync()
     2 {
     3     Console.WriteLine("start");
     4 
     5     Console.WriteLine("progress_1");
     6     await GetData();
     7 
     8     Console.WriteLine("progress_2");
     9     await GetData();
    10 
    11     Console.WriteLine("progress_3");
    12     await GetData();
    13 
    14     Console.WriteLine("end");
    15 }
    复制代码

    因为IL上面已经讲过,多个await的指令其实差不多,所以用另一种简单的方法:ILSpy来直接看翻译结果,需要在Options里把Decompile async method(async/await)关掉,如图:

    MoveNext的代码:

    复制代码
     1 void IAsyncStateMachine.MoveNext()
     2 {
     3     int num = this.<> 1__state;
     4     try
     5     {
     6         TaskAwaiter<string> taskAwaiter;
     7         switch (num)
     8         {
     9             case 0:
    10                 taskAwaiter = this.<> u__1;
    11                 this.<> u__1 = default(TaskAwaiter<string>);
    12                 this.<> 1__state = -1;
    13                 break;
    14             case 1:
    15                 taskAwaiter = this.<> u__1;
    16                 this.<> u__1 = default(TaskAwaiter<string>);
    17                 this.<> 1__state = -1;
    18                 goto IL_ED;
    19             case 2:
    20                 taskAwaiter = this.<> u__1;
    21                 this.<> u__1 = default(TaskAwaiter<string>);
    22                 this.<> 1__state = -1;
    23                 goto IL_157;
    24             default:
    25                 Console.WriteLine("start");
    26                 Console.WriteLine("progress_1");
    27                 taskAwaiter = Program.GetData().GetAwaiter();
    28                 if (!taskAwaiter.IsCompleted)
    29                 {
    30                     this.<> 1__state = 0;
    31                     this.<> u__1 = taskAwaiter;
    32                     this.<> t__builder.AwaitUnsafeOnCompleted < TaskAwaiter<string>, Program.< DisplayDataAsync > d__1 > (ref taskAwaiter, ref this);
    33                     return;
    34                 }
    35                 break;
    36         }
    37         taskAwaiter.GetResult();
    38         taskAwaiter = default(TaskAwaiter<string>);
    39         Console.WriteLine("progress_2");
    40         taskAwaiter = Program.GetData().GetAwaiter();
    41         if (!taskAwaiter.IsCompleted)
    42         {
    43             this.<> 1__state = 1;
    44             this.<> u__1 = taskAwaiter;
    45             this.<> t__builder.AwaitUnsafeOnCompleted < TaskAwaiter<string>, Program.< DisplayDataAsync > d__1 > (ref taskAwaiter, ref this);
    46             return;
    47         }
    48         IL_ED:
    49         taskAwaiter.GetResult();
    50         taskAwaiter = default(TaskAwaiter<string>);
    51         Console.WriteLine("progress_3");
    52         taskAwaiter = Program.GetData().GetAwaiter();
    53         if (!taskAwaiter.IsCompleted)
    54         {
    55             this.<> 1__state = 2;
    56             this.<> u__1 = taskAwaiter;
    57             this.<> t__builder.AwaitUnsafeOnCompleted < TaskAwaiter<string>, Program.< DisplayDataAsync > d__1 > (ref taskAwaiter, ref this);
    58             return;
    59         }
    60         IL_157:
    61         taskAwaiter.GetResult();
    62         taskAwaiter = default(TaskAwaiter<string>);
    63         Console.WriteLine("end");
    64     }
    65     catch (Exception exception)
    66     {
    67         this.<> 1__state = -2;
    68         this.<> t__builder.SetException(exception);
    69         return;
    70     }
    71     this.<> 1__state = -2;
    72     this.<> t__builder.SetResult();
    73 }
    复制代码

    还是比较容易理解,思路和单个await一样,这里通过goto的方式来控制流程,很聪明的做法,这样既可以跳转,又不影响taskAwaiter.IsCompleted为true时的直接运行。

    在讲AsyncVoidMethodBuilder.Create时讲到SynchronizationContext的用处是处理异常,那现在来看看AsyncVoidMethodBuilder的异常处理:

    复制代码
     1 internal static void ThrowAsync(Exception exception, SynchronizationContext targetContext)
     2 {
     3     var edi = ExceptionDispatchInfo.Capture(exception);
     4 
     5     if (targetContext != null)
     6     {
     7         try
     8         {
     9             targetContext.Post(state => ((ExceptionDispatchInfo)state).Throw(), edi);
    10             return;
    11         }
    12         catch (Exception postException)
    13         {
    14             edi = ExceptionDispatchInfo.Capture(new AggregateException(exception, postException));
    15         }
    16     }
    17 }
    复制代码

    看到了吧,把异常通过targetContext.Post的方式给到最开始的线程,这也是为什么在Task外面的try..catch能抓到异步异常的原因。

    总结

    好了,以上就是用IL来对async/await的分析,总结一下:

    async/await本质上只是一个语法糖,它并不产生线程,只是在编译时把语句的执行逻辑改了,相当于过去我们用callback,这里编译器帮你做了。线程的转换是通过SynchronizationContext来实现,如果做了Task.ConfigureAwait(false)操作,运行MoveNext时就只是在线程池中拿个空闲线程出来执行;如果Task.ConfigureAwait(true)-(默认),则会在异步操作前Capture当前线程的SynchronizationContext,异步操作之后运行MoveNext时通过SynchronizationContext转到目标之前的线程。一般是想更新UI则需要用到SynchronizationContext,如果异步操作完成还需要做大量运算,则可以考虑Task.ConfigureAwait(false)把计算放到后台算,防止UI卡死。

    另外还有在异步操作前做的ExecutionContext.FastCapture,获取当前线程的执行上下文,注意,如果Task.ConfigureAwait(false),会有个IgnoreSynctx的标记,表示在ExecutionContext.Capture里不做SynchronizationContext.Capture操作,Capture到的执行上下文用来在awaiter completed后给MoveNext用,使MoveNext可以有和前面线程同样的上下文。

    通过SynchronizationContext.Post操作,可以使异步异常在最开始的try..catch块中轻松捕获。

     
     
     
  • 相关阅读:
    mysql导出存储过程、函数、视图、触发器
    通过mk-table-checksum与pt-table-sync检查不同库两张表的一致性。
    Linux内核OOM机制的详细分析
    Linux虚拟内存(VM)相关参数解析
    mysqld异常重启后,自动启动应用srm进程
    利用python多线程执行远程linux上命令
    oracle数据库时常用的操作命令
    Oralce_DDL
    Oralce_PL_SQL
    mysqlbackup备份和还原
  • 原文地址:https://www.cnblogs.com/CrabMan/p/5436083.html
Copyright © 2020-2023  润新知