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

Posted 布鲁克石

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进阶篇:以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可以认

以上是关于进阶篇:以IL为剑,直指async/await的主要内容,如果未能解决你的问题,请参考以下文章

第126篇: 异步函数(async和await)

ReactNative进阶(三十六):ES6中async与await的使用方法详解

Async/Await是这样简化JavaScript代码的

[转] Async/Await替代Promise的6个理由

NET 中的 async/await 异步编程

浅谈async/await