有关async/await的实现背后
目录
开始用async/await
最近几个项目中涉及到了.Net Framework 4.5和WinRT,这才算用上了之前一直认为是新·语法糖的async/await异步编程方式。类似之前需要等待一段时间才能完成的的代码——比如网络访问、文件读写、新dialog中用户交互等——可以不用启动新的线程、等待回调、返回到UI线程后再更新界面,而是直接按照同步方式进行调用。代码的可读性会大大提高,这一点毋庸置疑,例如下面的(伪)代码:
void Download( string fileUri ) {
Networking.Download( fileUri, ( response ) => {
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => {
DownloadStatus = "Done!";
} );
} );
}
就可以转写为:
async void Download( string fileUri ) {
await Networking.Download( fileUri );
DownloadStatus = "Done!";
}
至少最后那堆括号没有了。实际上async/await提供的不仅是语义上更符合常理的用法。那么这是如何实现的?
[Symbian中的实现]
说点题外话。有过Symbian开发经验的程序员一定也遇到过异步方法需要同步调用的头疼情况,例如用户选取Gallery中的图片或者需要等待网络请求完成后再继续操作。由于Active Object的存在,Symbian中的单线程模拟异步方式实现起来麻烦得很,所以后来大家要么用局部TReqeustStatus+FOREVER循环,要么用User::WaitForRequest()的方式卡死当前上下文。结果就是导致了无数奇怪的Bug。这部分和本文其实没什么太大关系,只是因为当年被Symbian荼毒过深,不由自主想起来了而已:)。
新模型的基本概念
先从结论说起:async/await理论上讲还是C#语言层次的语法糖,在IL层面不涉及到新的指令;但它的实现并非“启动一个新线程去更新数据”,而是利用同步上下文实现了并行性(Concurrency),比起多线程来说,这个实现无论在时间还是空间消耗上,性能并不逊色[2]。
退一步说,不要说新的线程,async关键字本身甚至不会导致函数以异步方式运行,不管这个函数是否有Task或者Task<>返回值,它所做的就是标记内部可能会有await实现[3]。而在另一方面,await用于标记可等待(awaitable)的实现方法。简单说来就是在await出现的地方,把后面需要同步执行的代码作为“后续(continuation)”记录,当作异步方法的回调;这样异步方法调用部分将会和同步方法“分离(slicing)”开来,包装成数段代码,后面几节会很清晰地看到——后续的同步代码会作为delegate在异步操作完成后执行。
从IL入手
了解语法层面新关键字、功能的实现,ildasm永远是最好用的。大概在大前年还是前年,项目不太紧张的时候我跟着《Microsoft .Net IL汇编语言程序设计》和另外一本《IL Book》看了一段时间IL指令集,记了些笔记,还手写过点代码片段。可惜后来扔下了,因为越看越觉得过于细节接触IL指令集没什么大用,随用随查才是王道。基础指令还是应该记下来,以备不时之需,必须现在就需要了。这里我就不列出具体的指令作用,如果有需要的同学可以参考Under the covers of the async modifier and await operator in .NET 4.5 and C# Metro style applications这篇精彩的分析[4],里面用表格形式说明了具体类似于stloc, stfld, ldarg, ldloc, new等等指令和IL code中各field的作用。
那么先写出C#代码:
class AsyncClass {
public async void FooAsync() {
int i = 10;
var files = await ApplicationData.Current.LocalFolder.GetFilesAsync();
i = 20;
}
public async Task FooAsync2() {
int i = 10;
var files = await ApplicationData.Current.LocalFolder.GetFilesAsync();
i = 20;
return string.Empty;
}
}
用ildasm打开,大概的情况如图:
首先值得值得注意的是,就如同匿名方法、扩展方法一样,async/await并未引入任何新的IL指令,一切都是通过上层实现的。可以看到IL Assemler生成了两个形如< FooAsync >d__0的结构体,继续打开看看:
这个结构体从IAsyncStateMachine实现,包含很多field和两个方法MoveNext和SetStateMachine。这些field中包括执行上下文的this指针(AsyncClass)、async方法内用到的局部变量(files, i, j),再加上框架生成的状态机相关的类(awaiter等)。SetStateMachine明显是IAsyncStateMachine接口中定义的需要实现的方法,那么MoveNext打开看看(IL代码太多,故都折叠了):
.method private hidebysig newslot virtual final
instance void MoveNext() cil managed
{
.override [System.Threading.Tasks]System.Runtime.CompilerServices.IAsyncStateMachine::MoveNext
// 代码大小 213 (0xd5)
.maxstack 3
.locals init ([0] bool '<>t__doFinallyBodies',
[1] class [System.Runtime]System.Exception '<>t__ex',
[2] int32 CS$4$0000,
[3] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1> CS$0$0001,
[4] valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1> CS$0$0002,
[5] class [System.Runtime]System.Collections.Generic.IReadOnlyList`1 CS$0$0003)
.try
{
IL_0000: ldc.i4.1
IL_0001: stloc.0
IL_0002: ldarg.0
IL_0003: ldfld int32 BehindAsync.AsyncClass/'d__0'::'<>1__state'
IL_0008: stloc.2
IL_0009: ldloc.2
IL_000a: ldc.i4.s -3
IL_000c: beq.s IL_0014
IL_000e: ldloc.2
IL_000f: ldc.i4.0
IL_0010: beq.s IL_0019
IL_0012: br.s IL_001b
IL_0014: br IL_00a4
IL_0019: br.s IL_0065
IL_001b: br.s IL_001d
IL_001d: nop
IL_001e: ldarg.0
IL_001f: ldc.i4.s 10
IL_0021: stfld int32 BehindAsync.AsyncClass/'d__0'::'5__1'
IL_0026: call class [Windows]Windows.Storage.ApplicationData [Windows]Windows.Storage.ApplicationData::get_Current()
IL_002b: callvirt instance class [Windows]Windows.Storage.StorageFolder [Windows]Windows.Storage.ApplicationData::get_LocalFolder()
IL_0030: callvirt instance class [Windows]Windows.Foundation.IAsyncOperation`1> [Windows]Windows.Storage.StorageFolder::GetFilesAsync()
IL_0035: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1 [System.Runtime.WindowsRuntime]System.WindowsRuntimeSystemExtensions::GetAwaiter>(class [Windows]Windows.Foundation.IAsyncOperation`1)
IL_003a: stloc.3
IL_003b: ldloca.s CS$0$0001
IL_003d: call instance bool valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1>::get_IsCompleted()
IL_0042: brtrue.s IL_0083
IL_0044: ldarg.0
IL_0045: ldc.i4.0
IL_0046: stfld int32 BehindAsync.AsyncClass/'d__0'::'<>1__state'
IL_004b: ldarg.0
IL_004c: ldloc.3
IL_004d: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1> BehindAsync.AsyncClass/'d__0'::'<>u__$awaiter3'
IL_0052: ldarg.0
IL_0053: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder BehindAsync.AsyncClass/'d__0'::'<>t__builder'
IL_0058: ldloca.s CS$0$0001
IL_005a: ldarg.0
IL_005b: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::AwaitUnsafeOnCompleted>,valuetype BehindAsync.AsyncClass/'d__0'>(!!0&,
!!1&)
IL_0060: nop
IL_0061: ldc.i4.0
IL_0062: stloc.0
IL_0063: leave.s IL_00d3
IL_0065: ldarg.0
IL_0066: ldfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1> BehindAsync.AsyncClass/'d__0'::'<>u__$awaiter3'
IL_006b: stloc.3
IL_006c: ldarg.0
IL_006d: ldloca.s CS$0$0002
IL_006f: initobj valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1>
IL_0075: ldloc.s CS$0$0002
IL_0077: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1> BehindAsync.AsyncClass/'d__0'::'<>u__$awaiter3'
IL_007c: ldarg.0
IL_007d: ldc.i4.m1
IL_007e: stfld int32 BehindAsync.AsyncClass/'d__0'::'<>1__state'
IL_0083: ldloca.s CS$0$0001
IL_0085: call instance !0 valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1>::GetResult()
IL_008a: ldloca.s CS$0$0001
IL_008c: initobj valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.TaskAwaiter`1>
IL_0092: stloc.s CS$0$0003
IL_0094: ldarg.0
IL_0095: ldloc.s CS$0$0003
IL_0097: stfld class [System.Runtime]System.Collections.Generic.IReadOnlyList`1 BehindAsync.AsyncClass/'d__0'::'5__2'
IL_009c: ldarg.0
IL_009d: ldc.i4.s 20
IL_009f: stfld int32 BehindAsync.AsyncClass/'d__0'::'5__1'
IL_00a4: leave.s IL_00be
} // end .try
catch [System.Runtime]System.Exception
{
IL_00a6: stloc.1
IL_00a7: ldarg.0
IL_00a8: ldc.i4.s -2
IL_00aa: stfld int32 BehindAsync.AsyncClass/'d__0'::'<>1__state'
IL_00af: ldarg.0
IL_00b0: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder BehindAsync.AsyncClass/'d__0'::'<>t__builder'
IL_00b5: ldloc.1
IL_00b6: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetException(class [System.Runtime]System.Exception)
IL_00bb: nop
IL_00bc: leave.s IL_00d3
} // end handler
IL_00be: nop
IL_00bf: ldarg.0
IL_00c0: ldc.i4.s -2
IL_00c2: stfld int32 BehindAsync.AsyncClass/'d__0'::'<>1__state'
IL_00c7: ldarg.0
IL_00c8: ldflda valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder BehindAsync.AsyncClass/'d__0'::'<>t__builder'
IL_00cd: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::SetResult()
IL_00d2: nop
IL_00d3: nop
IL_00d4: ret
} // end of method 'd__0'::MoveNext
虽然一眼看到了实际的异步方法调用确实被包装在了MoveNext中,但这里面的处理略显复杂。跟着IL代码从头走下来是最简单直接的途径,无他。那么具体这个方法做了什么事?
首先可以看到IL_0003到0C段,状态机通过switch语句进行了状态__state检查:
- 是否同-3相等,如果是跳转到IL_0014处。这个跳转又直接br到了IL_00a4,直接退出了程序,那么这里应该是__state==COMPLETED一类的处理。
- 是否同0相等,如果是跳转到IL_0019处,接着跳转到IL_0065,用于处理最后一段同步代码。这里__state被赋值为-1,awaiter被初始化,等待调用。接下来awaiter.GetResult()调用,__state赋值-2,刚刚得到的Result被用于AsyncVoidMethodBuilder::SetResult(),这里应该是没有后续await方法的情况。函数返回,等待下一步调用(退出)。
- 是否既不为-3又不为0,如果是则跳转到IL_001b,接着跳转到IL_001d,用于处理第一段同步代码。这里实际的异步方法被调用。awaiter::get_IsCompleted()被检查是否为true,也就是说如果awaitable Task被完成了的话,直接跳转到刚刚awaiter.GetResult()的地方;如果未完成,则__state赋值为0,调用AsyncVoidMethodBuilder::AwaitUnsafeOnCompleted,传入awaiter和this作为参数,函数就此返回,等待下一步调用。
除了正常流程之外,对于执行期间的异常也进行了捕获。出现后__state赋值为-2,并且调用AsyncVoidMethodBuilder::SetException进行处理。
框架为了驱动整个流程,似乎初始时候的__state不能为0,这个猜测也在后面得到了印证。对于所生成结构体运转的结构和理解就是这样了,那么回到原先的AsyncClass处,打开第一个方法FooAsync():
.method public hidebysig instance void FooAsync() cil managed
{
.custom instance void [System.Diagnostics.Debug]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = ( 01 00 00 00 )
.custom instance void [System.Runtime]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [System.Runtime]System.Type) = ( 01 00 25 42 65 68 69 6E 64 41 73 79 6E 63 2E 41 // ..%BehindAsync.A
73 79 6E 63 43 6C 61 73 73 2B 3C 46 6F 6F 41 73 // syncClass+d__0..
// 代码大小 48 (0x30)
.maxstack 2
.locals init (valuetype BehindAsync.AsyncClass/'d__0' V_0,
valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder V_1)
IL_0000: ldloca.s V_0
IL_0002: ldarg.0
IL_0003: stfld class BehindAsync.AsyncClass BehindAsync.AsyncClass/'d__0'::'<>4__this'
IL_0008: ldloca.s V_0
IL_000a: call valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Create()
IL_000f: stfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder BehindAsync.AsyncClass/'d__0'::'<>t__builder'
IL_0014: ldloca.s V_0
IL_0016: ldc.i4.m1
IL_0017: stfld int32 BehindAsync.AsyncClass/'d__0'::'<>1__state'
IL_001c: ldloca.s V_0
IL_001e: ldfld valuetype [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder BehindAsync.AsyncClass/'d__0'::'<>t__builder'
IL_0023: stloc.1
IL_0024: ldloca.s V_1
IL_0026: ldloca.s V_0
IL_0028: call instance void [System.Threading.Tasks]System.Runtime.CompilerServices.AsyncVoidMethodBuilder::Startd__0'>(!!0&)
IL_002d: br.s IL_002f
IL_002f: ret
} // end of method AsyncClass::FooAsync
在此方法中,我们对于await XXXAsync()的调用被翻译成了AsyncVoidMethodBuilder的创建(000a)、初始化(000f-0023)和启动(0028)过程。可以注意到在IL_0016处,刚刚看到的__state被初始化为-1。生成的
用过IL代码了解流程可以保证准确性,但其可读性连差强人意都算不上。后面利用大牛们写的一些C#层次的分析文章,我会回顾一下上面提到的流程,以便更加清楚整个状态机是如何运转起来的。
回到C#代码
IL Spy的作者Daniel Grunwald在2.0版本中试着对async/await相关机制进行了反编译的支持,其中提到编译器生成的代码(compiler generation)类似于yield语句的代码结构,就如我们在上面IL代码中看到的一样,整个awaitable task运转过程是由状态机驱动的。只不过那篇Blog撰写时候IL Spy的async/await解析支持还在制作中,所以生成的C#代码并非完美。
下面则是一个更接近于实际代码的版本[7],整体的数据结构和代码流程和上面一节的分析没有太大区别。
[CompilerGenerated]
[StructLayout(LayoutKind.Auto)]
internal struct MethodAsyncStateMachine : IAsyncStateMachine
{
public int State;
public AsyncTaskMethodBuilder Builder;
public int Arg0;
public int Arg1;
public int Result;
private TaskAwaiter awaitor;
void IAsyncStateMachine.MoveNext()
{
try
{
if (this.State != 0)
{
this.awaitor = HelperMethods.MethodTask(this.Arg0, this.Arg1).GetAwaiter();
if (!this.awaitor.IsCompleted)
{
this.State = 0;
this.Builder.AwaitUnsafeOnCompleted(ref this.awaitor, ref this);
return;
}
}
else
{
this.State = -1;
}
this.Result = this.awaitor.GetResult();
}
catch (Exception exception)
{
this.State = -2;
this.Builder.SetException(exception);
return;
}
this.State = -2;
this.Builder.SetResult(this.Result);
}
[DebuggerHidden]
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
{
this.Builder.SetStateMachine(param0);
}
}
那么回想提一下,开始时候我们提到过同一个方法内的方法分片(slicing),也就是说不连续的await调用会被视为多段代码,同步调用亦是如此。那么在具体实现上,这些代码段是作为多个状态存在吗?试验一下即可,把FooAsync改为下面这样:
public async void FooAsync() {
int i = 10;
var files = await ApplicationData.Current.LocalFolder.GetFilesAsync();
i = 20;
var folders = await ApplicationData.Current.LocalFolder.GetFoldersAsync();
i = 30;
var items = await ApplicationData.Current.LocalFolder.GetItemsAsync();
int j = 0;
}
用ildasm打开后可以看到,整体的代码并没有出现奇怪的变化,只是随着代码量变大指令调用也多了(不贴了,因为太多,关键在于那个switch table),可视的变化包括:多了两个awaiter;__state的判断多了“1”和“2”两种情况。似乎可以推断出,在StateMachine Struct::MoveNext(),是将原有代码按照一段同步异步代码+一段同步代码组合起来作为一个State,并且State的序号从0开始。以每一段await调用为分界点,将代码切分,异步代码部分作为awaiter存在,后续的同步代码则用匿名函数包装作为回调,在awaiter返回后调用。结合之前的分析结果,__state对应到具体的状态上大概是:
- state == -3:直接结束(似乎只用于比较结果)
- state == -2:流程完毕
- state == -1:流程开始
- state >= 0:第state + 1段异步同步+同步代码
但从实际的生成代码来看,在MoveNext()中调用时视觉上和我们的分析刚好反过来,在switch语句中是一段同步代码跟着一段异步的,在case语句块中的逻辑类似于:执行同步代码、执行异步代码、检查是否Complete、更新__state(置为后续state或者-2)。实际的生成代码中,则是将struct本身指针的MoveNext()方法调用置于awaiter的完成回调中,利用递归实现了状态机的变化[7],更容易阅读和接近实际情况的代码也可以在[7]中看到。
await, awaiter, awaitable
什么是awaitable?顾名思义就是可以用await进行调用的对象。从上面几节的分析可以得出,XXXAsync函数之所以可用await关键字修饰,在于它返回了Task对象(或是Task
我们可以自己构建awaitable对象吗?当然可以,只不过.Net并未提供相关接口,都需要自己进行实现。我们在用var folders = await ApplicationData.Current.LocalFolder.GetFoldersAsync();调用方法的时候,实际上等同于
var operation = ApplicationData.Current.LocalFolder.GetFoldersAsync();
var task = operation.AsTask>();
var awaiter = task.GetAwaiter();
var folders = awaiter.GetResult();
这样一来,我们就知道awaitable对象必须返回一个awaiter,而awaiter需要信号量标识是否完成以及提供返回结果的接口——实现ICriticalNotifyCompletion:
- bool IsCompleted { get; }
- void OnCompleted(Action continuation); (这里的continuation就是刚刚提到MoveNext中异步代码后续的同步代码)
- TResult GetResult();
注意到没,这些Property和Function刚才都在生成代码中见过了。这是一个泛型版本,非泛型的版本仅仅是GetResult()不返回值。在[8]中已经有了实现INotifyCompletion的泛型/非泛型版本的完备版本,同时也有利用现有Task、TaskAwaiter对Action和Func直接进行扩展方法书写,让其支持await调用的例子,大家可以移步参考。
最后一点……Task
Task::ConfigureTask()拿到的ConfiguredTaskAwaitable到底有什么用?ConfigureTask方法接受一个bool值,如果为false则系统可能不会准确地返回到当前的上下文继续执行callback的同步代码,而是在系统认为合适的地方执行[5]。换句话说,如果你在UI线程利用AsTask().ConfirgureTask(false)进行了异步操作,那么要小心这行代码后面的同步代码是否修改了UI控件或是绑定的数据,因为它不一定会回到UI线程继续执行。
Task还提供了一对方法WhenAny()和WhenAll()以及它们的泛型版本,用来对多个同时进行的任务进行同步。从函数名字可以一目了然地清楚它们的作用:传入多个awaitable object后,可以实现类似使用Mutex或是ResetEvent的线程同步。
参考资料
[1] Asynchronous Programming with Async and Await (C# and Visual Basic), MSDN
[2] What is the cost of async/await?, Bnaya Eshet
[3] Async/Await FAQ, MSDN
[4] Under the covers of the async modifier and await operator in .NET 4.5 and C# Metro style applications, Pete Brown
[5] Diving deep with WinRT and await, Windows 8 app developer blog
[6] Decompiling Async/Await, Daniel Grunwald
[7] Understanding C# async / await (1) Compilation, Dixin
[8] Understanding C# async / await (2) Awaitable / Awaiter Pattern, Dixin
Great resources and tips for families here.
viagra from canada best place to buy generic viagra online viagra over the counter walmart
I enjoy your blog posts, saved to my bookmarks!
where can i buy viagra over the counter buy viagra online usa buy real viagra online
viagra from india mexican viagra best place to buy generic viagra online
viagra without a doctor prescription where to buy viagra price of viagra
viagra cost generic viagra walmart viagra over the counter
This is one very informative blog. I like the way you write and I will bookmark your blog to my favorites.
Thanks for sharing the information. I found the information very useful. That’s a awesome story you posted. I will come back to scan some more.
Spot on with this write-up, I truly believe this website requirements a lot much more consideration. I’ll probably be once more to read much much more, thanks for that info.
I’m so happy to read this. This is the type of manual that needs to be given and not the random misinformation that’s at the other blogs. Appreciate your sharing this best doc.
Very fine blog.
I think I might disagree with some of your analysis. Are the figures solid?
Wow, amazing blog layout! How long have you been blogging for? you made blogging look easy. The overall look of your site is great, as well as the content!
I would share your post with my sis.
I do believe your audience could very well want a good deal more stories like this carry on the excellent hard work.
Only a smiling visitor here to share the love (:, btw outstanding style and design .
https://sildenafilmg.shop/# viagra amazon
cost of amoxicillin 30 capsules order amoxicillin uk
https://zithromaxforsale.shop/# order zithromax without prescription
clomid pills otc buy clomid online uk 50mg
prednisone 20mg by mail order prednisone buying
https://prednisoneforsale.store/# price of prednisone 5mg
Woh I enjoy your content , saved to bookmarks!
doxycycline 100mg cost in india drug doxycycline
Most often since i look for a blog Document realize that the vast majority of blog pages happen to be amateurish. Not so,We can honestly claim for which you writen is definitely great and then your webpage rock solid.
where can i buy amoxicillin online amoxicillin from canada
darknet drugs asap market
Nice blog here! Also your web site loads up very fast! What host are you using? Can I get your affiliate link to your host? I wish my web site loaded up as fast as yours lol
prednisone 200 mg tablets prednisone brand name canada