大概从 Unity 2017 开始就正式支持 .Net 4.x 版本,很多新的语法都支持了,包括 dynamic 动态类型、async/await 异步模型,其中对 async/await 的支持尤其重要,虽然之前可以使用协程来进行异步编程,但是协程存在很多限制,最突出的两个问题是,没有返回值,以及异常处理问题,async/await的出现,很好的解决的这些问题,但是同时又带了一些新的问题。
async/await异步模型使用起来很简单,但是要真正的完全理解,其中涉及到很多难理解的概念,比如多线程、线程池、同步上下文、任务调度等等,对于初学者,这些单个概念都不太好理解,MSDN 上面提供的资料也不是很多,同时,本人的能力水平有限,其中难免存在纰漏,但是我还是尽可能的将我的理解表述清楚,便于读者理解。
下面文章从一个简单的例子出发,首先了解一下 async/await 是如何实现异步的。
async/await是怎么工作的
在 C# 中,async/await语法的作用其实只是告诉编译器,它所修饰的方法是一个异步方法,需要编译器进行相应的转化,还是直接看一个简单示例:
| 1 | class Program | 
运行结果如下:
| 1 | Before: 1 | 
上面代码比较简单,AsyncSample 类里面包含一个 DoAsync() 方法,可以看到 await 语句前后代码块不在同一个线程内执行,这就很有意思了,为了研究它的原理,先看一下它的反编译结果,我这里使用的是 ILSpy,下面结果是对反编译字段进行重新命名的,另外反编译的时候选择 C# 4.0 语法:
| 1 | internal class Program | 
对比反编译结果和源码,可以法线变化的只是 AsyncSample 类,变化主要有三点:
- DoAsync()方法去掉了- async修饰;
- DoAsync()方法实现完全被重写;
- 增加了 DOAsyncStateMechine嵌套类,这是一个状态机类型,状态机的主体是MoveNext(),因为源码里只有一个await,所以这里有三个状态,如下图所示:
下面梳理一下状态机执行流程,从 DoAsync() 开始:
- 首先构建状态机,初始 builder成员,并将状态机的初始状态设置为-1;
- 然后调用 builder.Start(ref stateMachine)该方法会调用状态机的MoveNext()函数;
- 在 MoveNext()中,由于初始state == -1因此进入第一阶段,执行Before部分代码;
- 然后紧接着进入阶段二,执行 Task.Delay(1000),该方法返回一个Task类型对象,调用Task.GetAwaiter()获取TaskAwaiter对象,然后通过IsCompleted属性判断任务是否完成,如果完成,则直接跳转到阶段三;如果没有完成,则会将state设置为0,然后调用builder.builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine)并退出状态机,它会在任务结束之后,再次调用状态机;
- 任务完成后,再次调用状态机的 MoveNext(),此时直接进入到阶段三,至此整个状态机执行完毕。
通过上面的过程,大致的了解了编译器对 async/await 代码做了什么,能够知道为什么一个 await 语句就能够做到异步执行,但是这里还没有解决开头提出的问题,为什么 await 语句前后执行线程不同? TaskAwaiter 内部是如何工作的? 以及 builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine) 内部是如何实现的?
这里为了方便理解,采用了Task.Delay来进行演示,如果换成Task.Run(Action)又会稍微复杂一点,为了研究明白这些问题,后序将会按照以下两大块来分别研究:
- 任务是如何异步的执行的?
- 任务是如何检测结束的?
- 任务结束之后,是如何执行后续任务的?
总结
通过以上的过程,能够初步了解 async/await是怎么一个工作机制,就是通过状态机定义一系列的状态转换,awaiter 语句前后代码安排在特定的状态执行。