进阶篇:以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的主要内容,如果未能解决你的问题,请参考以下文章