大概从 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
语句前后代码安排在特定的状态执行。