什么是异步?

异步是一种任务执行的机制或者说方式,它的目的在于解决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/