前前言
原文博客链接
由于CSDN界面越来越烂,因此自己搭建了一个博客网站,上面的链接是我自己的个人博客网站地址,希望大家可以去上面阅读我的文章,CSDN只是用来备份数据
http://www.lizhenghao.site/blog/2022/01/12/152
个人的博客首页:
前言
对于我来说,Task+await+async已经用的很熟练了,像之前也写过一篇文章http://www.lizhenghao.site/blog/2021/12/30/89 讲了一下怎么用Task和TaskFactory实现限制并行任务数量的多线程操作,但是自己对底层的实现机制一直比较懵懂,下面自己写个demo再用dnspy看一下源码(dnspy,永远滴神!)
来个demo
internal class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("begin");
await Task.Run(() =>
{
Console.WriteLine("inside");
Thread.Sleep(5000);
});
Console.WriteLine("end");
while (true)
{
Thread.Sleep(1000);
Console.WriteLine("Wait 1 second");
}
}
}
首先来段非常简单的代码,下面我们用dnspy看一下反编译后的代码情况
注意这里如果用ilspy得把环境调整到C# 1.0版本,否则版本太高直接给反编译的跟源代码一样了,如果是用dnspy,记得在设置里把反编译异步方法、反编译匿名方法、反编译表达树这些选项关掉,否则反编译之后很难看明白
下面我们先一段一段把这个代码进行分解,半蒙半猜来推导一下这里面实现了啥逻辑
首先上来Main函数,基本原先的代码就被改的面目全非了,经过一番分析后,发现首先是在原本的Program内部插入了一个内部类<Main>d__0
,这里我们就叫它状态机类吧,因为这个类继承了IAsyncStateMachine
接口,从这个接口的名字也可以很容易就猜出来了,其实async await的实现机制就是靠状态机,再往下看,就是调用了一个builder成员的Start方法,这里暂且不管这个Start内部啥逻辑,我们就知道传入的是一个状态机类的对象,接下来我们单步调试一下看看start之后调用到了哪里:
没想到程序一运行调用的竟然是<Main>
而不是Main
,不知道为啥,但是也不重要了,看下面调用的堆栈和当前的断点位置我们会发现,果然在Start之后就进入了MoveNext
函数,好了,下面再来分析一下MoveNext
函数干了啥
具体的流程看上图中的注释就可以猜个大概了,下面就是这个AwaitUNSageOnCompleted这个函数看不懂是干啥的,还好有注释
简单翻译下,就是说在awaiter完成的之后调用状态机的MoveNext函数继续执行,我们还是看一下里面具体是咋实现的
这下也很容易理解了,其实await就是把整个await这个函数里面的代码分成了三个部分:
先执行了await之前的代码,之后用Task执行内部的代码,立刻return,直到await内的代码执行完了再执行await后面的代码
我们再修改一下我们的demo,像下面这个样子:
在await之前我先输出一段日志到控制台,接着再await,我们看一下反编译后的源码是啥样子:
!!!竟然是直接把await之前的代码全部放到return之前去执行了…因此函数内部的分界线就是看await在什么地方。
总结一下
- async、await内部其实是靠状态机机制实现的,在await之前的代码执行完了之后就把对应Task完成的事件订阅一下,在事件触发时再次修改状态,从而执行MoveNext的下面await之后的代码。
- 为啥不会卡主线程?主要是在Task.Run之后直接return了,把后面的代码放到了下次再调用MoveNext的时候执行,这个执行的线程在控制台应用程序时不一定是主线程,而在WPF、Winform等带UI的项目中,是由主线程来完成后续逻辑执行的,这一点也是我自己实验过的。
关于以上的所有思路,都是靠自己阅读了一下源码了解的,如果有错误的地方欢迎指出
- https://www.youtube.com/watch?v=il9gl8MH17s 这里推荐一下我在油管搜到的一个讲async await很好的视频,推荐大家也去学习一下,视频长度25分钟
- 还有朝夕教育的Eleven老师也有个录播讲 Async await源码, 就是视频比较长,估计2,3小时,如果需要百度云资源的可以留言我,晚上下班了可以发你学习一下