读懂IL
先说说学IL有什么用,有人可能觉得这玩意平常写代码又用不上,学了有个卵用。到底有没有卵用呢,暂且也不说什么学了可以看看一些语法糖的实现,或对.net理解更深一点这些虚头巴脑的东西。最重要的理由就是一个:当面试官看你简历上写着精通C#时,问你一句:
"懂不懂IL?"
怎么回答?
"不好意思,那东西没什么卵用,所以我没学。"
还是
"还行,可以探讨一下。"
你觉得哪个回答好呢,答得好才更有底气要到更多的薪资,多个几千块也说不定,而这只不过花上不到半小时学习就可以跟面试官吹上一阵了,很实用,有没有。
为什么取这个标题呢,记得很久之前看过一篇文章,叫"正则表达式30分钟入门教程",学正则最重要的就是记住各个符号的含义。个人觉得相比难以直接看出实际意义的正则符号如"w","d","*","?","{}[]"等,IL的指令要容易得多。很多人见到IL一大堆的指令,和汇编一样,就感觉头大不想学了。其实IL本身逻辑很清楚,主要是把指令的意思搞明白就好办了。记指令只要记住几个规律就好,我把它们分为三类。
第一类 :直观型
这一类的特点是一看名字就知道是干嘛的,不需要多讲,如下:
名称 |
说明 |
Add |
将两个值相加并将结果推送到计算堆栈上。 |
Sub |
从其他值中减去一个值并将结果推送到计算堆栈上。 |
Div |
将两个值相除并将结果作为浮点(F 类型)或商(int32 类型)推送到计算堆栈上。 |
Mul |
将两个值相乘并将结果推送到计算堆栈上。 |
Rem |
将两个值相除并将余数推送到计算堆栈上。 |
Xor |
计算位于计算堆栈顶部的两个值的按位异或,并且将结果推送到计算堆栈上。 |
And |
计算两个值的按位"与"并将结果推送到计算堆栈上。 |
Or |
计算位于堆栈顶部的两个整数值的按位求补并将结果推送到计算堆栈上。 |
Not |
计算堆栈顶部整数值的按位求补并将结果作为相同的类型推送到计算堆栈上。 |
Dup |
复制计算堆栈上当前最顶端的值,然后将副本推送到计算堆栈上。 |
Neg |
对一个值执行求反并将结果推送到计算堆栈上。 |
Ret |
从当前方法返回,并将返回值(如果存在)从调用方的计算堆栈推送到被调用方的计算堆栈上。 |
Jmp |
退出当前方法并跳至指定方法。 |
Newobj |
New Object创建一个值类型的新对象或新实例,并将对象引用推送到计算堆栈上。 |
Newarr |
New Array将对新的从零开始的一维数组(其元素属于特定类型)的对象引用推送到计算堆栈上。 |
Nop |
如果修补操作码,则填充空间。尽管可能消耗处理周期,但未执行任何有意义的操作。Debug下的 |
Pop |
移除当前位于计算堆栈顶部的值。 |
Initobj |
Init Object将位于指定地址的值类型的每个字段初始化为空引用或适当的基元类型的 0。 |
Isinst |
Is Instance测试对象引用是否为特定类的实例。 |
Sizeof |
将提供的值类型的大小(以字节为单位)推送到计算堆栈上。 |
Box |
将值类转换为对象引用。 |
Unbox |
将值类型的已装箱的表示形式转换为其未装箱的形式。 |
Castclass |
尝试将引用传递的对象转换为指定的类。 |
Switch |
实现跳转表。 |
Throw |
引发当前位于计算堆栈上的异常对象。 |
Call |
调用由传递的方法说明符指示的方法。 |
Calli |
通过调用约定描述的参数调用在计算堆栈上指示的方法(作为指向入口点的指针)。 |
Callvirt |
对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。 |
强调一下,有三种call,用的场景不太一样:
Call:常用于调用编译时就确定的方法,可以直接去元数据里找方法,如静态函数,实例方法,也可以call虚方法,不过只是call这个类型本身的虚方法,和实例的方法性质一样。另外,call不做null检测。
Calli: MSDN上讲是间接调用指针指向的函数,具体场景没见过,有知道的朋友望不吝赐教。
Callvirt: 可以调用实例方法和虚方法,调用虚方法时以多态方式调用,不能调用静态方法。Callvirt调用时会做null检测,如果实例是null,会抛出NullReferenceException,所以速度上比call慢点。
第二类:加载(ld)和存储(st)
我们知道,C#程序运行时会有线程栈把参数,局部变量放上来,另外还有个计算栈用来做函数里的计算。所以把值加载到计算栈上,算完后再把计算栈上的值存到线程栈上去,这类指令专门干这些活。
比方说 ldloc.0:
这个可以拆开来看,Ld打头可以理解为Load,也就是加载;loc可以理解为local variable,也就是局部变量,后面的 .0表示索引。连起来的意思就是把索引为0的局部变量加载到计算栈上。对应的 ldloc.1就是把索引为1的局部变量加载到计算栈上,以此类推。
知道了Ld的意思,下面这些指令 也就很容易理解了。
ldstr = load string,
ldnull = load null,
ldobj = load object,
ldfld = load field,
ldflda = load field address,
ldsfld = load static field,
ldsflda = load static field address,
ldelem = load element in array,
ldarg = load argument,
ldc 则表示加载数值,如ldc.i4.0,
关于后缀
.i[n]:[n]表示字节数,1个字节是8位,所以是8*n的int,比如i1, i2, i4, i8,i1就是int8(byte), i2是int16(short),i4是int32(int),i8是int64(long)。
相似的还有.u1 .u2 .u4 .u8 分别表示unsigned int8(byte), unsigned int16(short), unsigned int32(int), unsigned int64(long);
.R4,.R8 表示的是float和double。
.ovf (overflow)则表示会进行溢出检查,溢出时会抛出异常;
.un (unsigned)表示无符号数;
.ref (reference)表示引用;
.s (short)表示短格式,比如说正常的是用int32,加了.s的话就是用int8;
.[n] 比如 .1,.2 等,如果跟在i[n]后面则表示数值,其他都表示索引。如 ldc.i4.1就是加载数值1到计算栈上,再如ldarg.0就是加载第一个参数到计算栈上。
ldarg要特别注意一个问题:如果是实例方法的话ldarg.0加载的是本身,也就是this,ldarg.1加载的才是函数的第一个参数;如果是静态函数,ldarg.0就是第一个参数。
与ld对应的就是st,可以理解为store,意思是把值从计算栈上存到变量中去,ld相关的指令很多都有st对应的,比如stloc, starg, stelem等,就不多说了。
第三类:比较指令,比较大小或判断bool值
有一部分是比较之后跳转的,代码里的 if 就会产生这些指令,符合条件则跳转执行另一些代码:
以b开头:beq, bge, bgt, ble, blt, bne
先把b去掉看看:
eq: equivalent with, ==
ge: greater than or equivalent with , >=
gt: greater than , >
le: less than or equivalent with, <=
lt: less than, <
ne: not equivalent with, !=
这样是不是很好理解了,beq IL_0005就是计算栈上两个值相等的话就跳转到IL_0005, ble IL_0023是第一个值小于或等于第二个值就跳转到IL_0023。
以br(break)开头:br, brfalse, brtrue,
br是无条件跳转;
brfalse表示计算栈上的值为 false/null/0 时发生跳转;
brtrue表示计算栈上的值为 true/非空/非0 时发生跳转
还有一部分是c开头,算bool值的,和前面b开头的有点像:
ceq 比较两个值,相等则将 1 (true) 推到栈上,否则就把 0 (false)推到栈上
cgt 比较两个值,第一个大于第二个则将 1 (true) 推到栈上,否则就把 0 (false)推到栈上
clt 比较两个值,第一个小于第二个则将 1 (true) 推到栈上,否则就把 0 (false)推到栈上
以上就是三类常用的,把这些搞明白了,IL指令也就理解得七七八八了。就像看文章一样,认识大部分字后基本就不影响阅读了,不认识的猜下再查下,下次再看到也就认得了。
例子
下面看个例子,随手写段简单的代码,是否合乎逻辑暂不考虑,主要是看IL:
源代码:
1 using System; 2 3 namespace ILLearn 4 { 5 class Program 6 { 7 const int WEIGHT = 60; 8 9 static void Main(string[] args) 10 { 11 var height = 170; 12 13 People people = new Developer("brook"); 14 15 var vocation = people.GetVocation(); 16 17 var healthStatus = People.IsHealthyWeight(height, WEIGHT) ? "healthy" : "not healthy"; 18 19 Console.WriteLine($"{vocation} is {healthStatus}"); 20 21 Console.ReadLine(); 22 } 23 } 24 25 abstract class People 26 { 27 public string Name { get; set; } 28 29 public abstract string GetVocation(); 30 31 public static bool IsHealthyWeight(int height, int weight) 32 { 33 var healthyWeight = (height - 80) * 0.7; 34 return weight <= healthyWeight * 1.1 && weight >= healthyWeight * 0.9; //标准体重是 (身高-80) * 0.7,区间在10%内都是正常范围 35 } 36 } 37 38 class Developer : People 39 { 40 public Developer(string name) 41 { 42 Name = name; 43 } 44 45 public override string GetVocation() 46 { 47 return "Developer"; 48 } 49 } 50 }
在命令行里输入:csc /debug- /optimize+ /out:program.exe Program.cs
打开IL查看工具:C:Program Files (x86)Microsoft SDKsWindowsv10.0AinNETFX 4.6 Toolsildasm.exe,不同版本可能目录不太一样。打开刚编译的program.exe文件,如下:
双击节点就可以查看IL,如:
Developer的构造函数:
1 .method public hidebysig specialname rtspecialname 2 instance void .ctor(string name) cil managed 3 { 4 // 代码大小 14 (0xe) 5 .maxstack 8 6 IL_0000: ldarg.0 //加载第1个参数,因为是实例,而实例的第1个参数始终是this 7 IL_0001: call instance void ILLearn.People::.ctor() //调用基类People的构造函数,而People也会调用Object的构造函数 8 IL_0006: ldarg.0 //加载this 9 IL_0007: ldarg.1 //加载第二个参数也就是name 10 IL_0008: call instance void ILLearn.People::set_Name(string) //调用this的 set_Name, set_Name这个函数是编译时为属性生成的 11 IL_000d: ret //return 12 } // end of method Developer::.ctor
Developer的GetVocation:
1 .method public hidebysig virtual instance string //虚函数 2 GetVocation() cil managed 3 { 4 // 代码大小 6 (0x6) 5 .maxstack 8 //最大计算栈,默认是8 6 IL_0000: ldstr "Developer" //加载string "Developer" 7 IL_0005: ret //return 8 } // end of method Developer::GetVocation
People的IsHealthyWeight:
1 .method public hidebysig static bool IsHealthyWeight(int32 height, //静态函数 2 int32 weight) cil managed 3 { 4 // 代码大小 52 (0x34) 5 .maxstack 3 //最大计算栈大小 6 .locals init ([0] float64 healthyWeight) //局部变量 7 IL_0000: ldarg.0 //加载第1个参数,因为是静态函数,所以第1个参数就是height 8 IL_0001: ldc.i4.s 80 //ldc 加载数值, 加载80 9 IL_0003: sub //做减法,也就是 height-80,把结果放到计算栈上,前面两个已经移除了 10 IL_0004: conv.r8 //转换成double,因为下面计算用到了double,所以要先转换 11 IL_0005: ldc.r8 0.69999999999999996 //加载double数值 0.7, 为什么是0.69999999999999996呢, 二进制存不了0.7,只能找个最相近的数 12 IL_000e: mul //计算栈上的两个相乘,也就是(height - 80) * 0.7 13 IL_000f: stloc.0 //存到索引为0的局部变量(healthyWeight) 14 IL_0010: ldarg.1 //加载第1个参数 weight 15 IL_0011: conv.r8 //转换成double 16 IL_0012: ldloc.0 //加载索引为0的局部变量(healthyWeight) 17 IL_0013: ldc.r8 1.1000000000000001 //加载double数值 1.1, 看IL_0010到IL_0013,加载了3次,这个函数最多也是加载3次,所以maxstack为3 18 IL_001c: mul //计算栈上的两个相乘,也就是 healthyWeight * 1.1, 这时计算栈上还有两个,第一个是weight,第二个就是这个计算结果 19 IL_001d: bgt.un.s IL_0032 //比较这两个值,第一个大于第二个就跳转到 IL_0032,因为第一个大于第二个表示第一个条件weight <= healthyWeight * 1.1就是false,也操作符是&&,后面没必要再算,直接return 0 20 IL_001f: ldarg.1 //加载第1个参数 weight 21 IL_0020: conv.r8 //转换成double 22 IL_0021: ldloc.0 //加载索引为0的局部变量(healthyWeight) 23 IL_0022: ldc.r8 0.90000000000000002 //加载double数值 0.9 24 IL_002b: mul //计算栈上的两个相乘,也就是 healthyWeight * 0.9, 这时计算栈上还有两个,第一个是weight,第二个就是这个计算结果 25 IL_002c: clt.un //比较大小,第一个小于第二个则把1放上去,否则放0上去 26 IL_002e: ldc.i4.0 //加载数值0 27 IL_002f: ceq //比较大小,相等则把1放上去,否则放0上去 28 IL_0031: ret //return 栈顶的数,为什么没用blt.un.s,因为IL_0033返回的是false 29 IL_0032: ldc.i4.0 //加载数值0 30 IL_0033: ret //return 栈顶的数 31 } // end of method People::IsHealthyWeight
主函数Main:
1 .method private hidebysig static void Main(string[] args) cil managed 2 { 3 .entrypoint //这是入口 4 // 代码大小 67 (0x43) 5 .maxstack 3 //大小为3的计算栈 6 .locals init (string V_0, 7 string V_1) //两个string类型的局部变量,本来还有个people的局部变量,被release方式优化掉了,因为只是调用了people的GetVocation,后面没用,所以可以不存 8 IL_0000: ldc.i4 0xaa //加载int型170 9 IL_0005: ldstr "brook" //加载string "brook" 10 IL_000a: newobj instance void ILLearn.Developer::.ctor(string) //new一个Developer并把栈上的brook给构造函数 11 IL_000f: callvirt instance string ILLearn.People::GetVocation() //调用GetVocation 12 IL_0014: stloc.0 //把上面计算的结果存到第1个局部变量中,也就是V_0 13 IL_0015: ldc.i4.s 60 //加载int型60 14 IL_0017: call bool ILLearn.People::IsHealthyWeight(int32, //调用IsHealthyWeight,因为是静态函数,所以用call 15 int32) 16 IL_001c: brtrue.s IL_0025 //如果上面返回true的话就跳转到IL_0025 17 IL_001e: ldstr "not healthy" //加载string "not healthy" 18 IL_0023: br.s IL_002a //跳转到IL_002a 19 IL_0025: ldstr "healthy" //加载string "healthy" 20 IL_002a: stloc.1 //把结果存到第2个局部变量中,也就是V_1, IL_0017到IL_002a这几个指令加在一起用来计算三元表达式 21 IL_002b: ldstr "{0} is {1}" //加载string "{0} is {1}" 22 IL_0030: ldloc.0 //加载第1个局部变量 23 IL_0031: ldloc.1 //加载第2个局部变量 24 IL_0032: call string [mscorlib]System.String::Format(string, //调用string.Format,这里也可以看到C# 6.0的语法糖 $"{vocation} is {healthStatus}",编译后的结果和以前的用法一样 25 object, 26 object) 27 IL_0037: call void [mscorlib]System.Console::WriteLine(string) //调用WriteLine 28 IL_003c: call string [mscorlib]System.Console::ReadLine() //调用ReadLine 29 IL_0041: pop 30 IL_0042: ret 31 } // end of method Program::Main
很简单吧,当然,这个例子也很简单,没有事件,没有委托,也没有async/await之类,这些有兴趣的可以写代码跟一下,这几种都会在编译时插入也许你不知道的代码。
就这么简单学一下,应该差不多有底气和面试官吹吹牛逼了。
结束
IL其实不难,有没有用则仁者见仁,智者见智,有兴趣就学一下,也花不了多少时间,确实也没必要学多深,是吧。
当然,也是要有耐心的,复杂的IL看起来还真是挺头痛。好在有工具ILSpy,可以在option里选择部分不反编译来看会比较简单些。
进阶篇:以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块中轻松捕获。