C#精要 - 异步篇
什么是异步?
异步是一种任务执行的机制或者说方式,它的目的在于解决I/O等待会阻塞线程这个问题(最常见的就是GUI线程阻塞造成画面卡顿),它的实现依托于硬件底层的IRP(I/O Request Packet),它的本质其实是回调。
我可以使用比如 ReadAsync + Task.ContinueWith 的组合,来实现一个异步实践;
而更简单的方式是通过微软后续推出 .net4.5 的 async-await 这套关键词来实践。
异步函数 async-await
异步函数,实际通过 核心类TaskAwaiter + 状态机 实现。
核心类 TaskAwaiter
这个类比较简单,每个异步Task都有。我把它看作黑盒不细究,只看对外接口:
- IsCompleted 属性:表示Task是否完成
- GetResult() 方法:结束异步任务完成的等待
- UnsafeOnCompleted(Action) 方法:设置延续任务
使用方法
await必须在有async标记的方法内使用。如果async方法内部没有await,那它就和同步方法一样执行。
如果执行中遇到了await,就把需要await执行的Task交给线程池来执行,而原来那个线程则退出async方法的方法体,去执行外部的后续代码,直到await的Task执行完毕返回结果后,这个线程会回到方法体await处继续执行。整体流程看上去很像ContinueWith。
接下来研究编译器做的事。
外部调用层
1.首先编译器看到方法有async标记,就会为其生成一个实现了IAsyncStateMachine接口的类,这个接口意味着它是一个异步状态机。
2.async方法自身则会被标记AsyncStateMachine特性,意味着这是个异步方法。
3.async方法内部则是new了一个异步状态机实例,先初始化,然后调用它的Start方法来启动状态机。Start方法内部主要是调用了状态机的MoveNext方法。
4.最后将指示方法运行状态的builder.Task
对象 return 。
内部状态机层
每个异步状态机都有2个核心字段和一个核心方法:
- builder:负责异步相关的操作,是方法实现异步执行的核心
- state:状态机的当前状态,初始化时赋值为 -1。
- MoveNext方法:状态机切换状态、执行任务、设置延续任务的方法
我们直接讲MoveNext方法。
第一次MoveNext时,我把它分成3块流程:
1.第一次进入状态机时state!=0
,await之前的代码被包裹到了MoveNext方法体内,照常顺序执行。
2.await那一行的代码,变成了获取任务的awaiter Task.GetAwaiter()
。然后对任务的awaiter进行判断:
- 如果awaiter.IsCompleted 为true,意味着任务已经执行完了,执行第3步。
- 如果awaiter.IsCompleted 为false,意味着任务还未执行完(一般初始化完第一次进来,都是false)那就会做以下操作:
将state赋值为0,将awaiter存到自身字段内供后续使用,然后设置延续任务builder.AwaitUnsafeOnCompleted
,设置完之后会return掉而不执行第3步。
延续任务内部怎么设置的比较复杂,我觉得不需要理解深入,理解为调了awaiter的接口就行了,反正最终效果是await的任务完成后再次调用MoveNext转动状态机。
3.结束awaiter TaskAwaiter.GetResult()
。执行原先await那一行之后的后续代码。执行完成后,将state赋值为-2,并为builder标记任务成功AsyncTaskMethodBuilder.SetResult()
。
如果不是第一次MoveNext,就会省去1、2步:
如果await的任务完成,就会触发延续任务:再次调用MoveNext。但是和第一次进不一样,此时因为state==0
会跳过第1步第2步,将state设置为-1之后直接跳入第3步。
多层嵌套async-await
其实实现也只是多层嵌套异步状态机而已,是一样的。
一个async中多个await
在同一个异步状态机中,生成更多的awaiter、更多的state。
state的-2(完成)、-1(初始)是定好的,所以只会从0开始生成,而有几个await就有几个awaiter、state。
一旦state多起来,内部就不再 if-else 了,而是 switch-case + goto 。
上下文
接触的主要是,
SynchronizationContext 同步上下文:捕获提供在各种同步模型中传播同步上下文的基本功能。它有一个Post()
虚方法,Winform、WPF等等会重写它,让它被调用时能产生不同的过程,但目的都是一个:实现使用GUI线程执行Post过去的委托。
ExecutionContext 执行上下文:流动。在执行委托时恢复另一个线程的状态环境。
WPF中使用 async await
上面我们都是通过控制台举的例子,这是没有任何SynchronizationContext
的,但是WPF(Winform同理)就不同了,在UI线程中,它拥有属于自己的DispatcherSynchronizationContext
。
这个工作中有体会,就是View.xmal.cs
文件内写诸如点击事件的async-await,延续任务会默认借调GUI线程而非await内分配的任务池线程。。
而我可以改成使用SynchronizationContext.Post()
来实现类似的效果。
怎么用
用SynchronizationContext可以实现确定性使用GUI线程来执行委托,我的理解只到此。
// TODO 光看不行,还是得自己实现一套简单的Task封装才行,以后参考UniTask。
参考:
https://www.cnblogs.com/xiaoxiaotank/p/14303803.html
https://www.cnblogs.com/xiaoxiaotank/p/13666913.html
https://codingcodingk.top/2022/01/14/Tech/CSharp/CLR-Via-CSharp/cp7/