C# 的 Await 到底做了什么
结论
为了节约观者时间,把结论先放在这里。如果你看懂了,就可以忽略文章内容。
await 的语义是:等待 Task 执行完成
async 的语义是:声明异步函数,由线程池来执行该任务。
当一个执行过程中有 await 时(async函数),编译器会为之生成一个状态机。每一个 await 任务都是状态机的一个状态,这些状态根据 await语句 的顺序依次切换,只有当上一个 await 任务完成时,状态才会转移/切换。编译器会自动处理异步函数中的执行语句,将他们放置在状态机的不同位置。
状态机的目的是保证 await 的任务按照程序编写的顺序依次完成 获取完成状态,而且不会阻塞主线程。而具体执行异步任务的是 dotnet 线程池。await 并不会创建线程,而且也不会管理异步任务的实际执行顺序,他只管理“提交任务”的顺序。
1. 创建一个异步任务 Task
Task 是C#对异步任务的建模,异步任务是没有参数的。
Task 包含一个委托,也就是异步逻辑,以及获取执行状态的属性。
Task mt = new Task(()=>{
int i = 10;
while (i > 0)
{
Console.WriteLine($"mt: {i}");
i--;
Thread.Sleep(1000);
}
});
mt.Start(); // 启动异步任务(将任务提交给线程池)
Console.WriteLine("任务启动!");
当 Task 任务启动后,任务被交给线程池执行,但是线程池有自己的执行计划,并不一定会立即分配线程并执行此任务。而“启动”仅仅做提交,主线程不会被阻塞。
2. 等待异步任务
Task 包含一个 Awaiter 等待器,是主线程中与线程池沟通的桥梁。通过等待器,可以获取任务完成状态,可以设定完成后回调。使用 Task.GetAwaiter()
即可获取异步实例的等待器。
等待器也可以阻塞主线程直到异步任务完成。使用 Awaiter.GetResult()
方法即可。
TaskAwaiter at = mt.GetAwaiter();
Console.WriteLine(at.IsCompleted);
at.OnCompleted(()=>{Console.WriteLine("任务完成事件")});
如果主线程有操作必须等待此异步任务完成,则调用 GetResult()
来阻塞主线程。
at.GetResult(); // 阻塞等待
Console.WriteLine("任务完成!");
3. 多个异步任务的完成顺序
当有多个异步任务同时启动后,我们无法决定他们的完成顺序,即便线程池按照任务提交顺序来分配优先级,但也无法确定任务的执行时间。
假设,任务1需要从互联网下载文件,任务2则将文件打印。任务2必须在任务1完成后再执行。
那么只能通过等待器来依次启动并等待,代码如下
Task task_http = Task.Run(()=>{ /* 下载文件 */ });
Task task_print = Task.Run(()=>{ /* 打印 */ });
task_http.Start();
task_http.GetAwaiter().GetResult();
task_print.Start();
task_print.GetAwaiter().GetResult();
这样虽然保证了两个任务能顺利进行,但调用 GetResult 会阻塞主线程。
这就让“异步”失去了意义。
只要把这两个有关系的异步任务合并成一个任务,不就可以了嘛?
Task MainTask = new Task(() =>
{
var t1 = new Task(() =>
{
Console.WriteLine("Task http Begin");
Thread.Sleep(4000);
});
var t2 = new Task(() =>
{
Console.WriteLine("Task print Begin");
Thread.Sleep(1000);
});
t1.Start();
var awaiter = t1.GetAwaiter();
awaiter.OnCompleted(()=>{Console.WriteLine("download completed");});
awaiter.GetResult();
t2.Start();
t2.GetAwaiter().GetResult();
});
MainTask.Start(); // 联合任务启动!
这样当然可以,但这会多添加一个线程来管理“下载打印联合任务”,这个管理线程会被阻塞。
4. 状态机
dotnet 采用 有限状态机 模型来处理 包含异步关键字的方法。
有限状态机模型中,把每个 await 任务视为一个 状态,当当前任务(状态)完成后,会进入下一个 任务。
任何包含 await 的方法都将被 dotnet 自动创建状态机。
下面是一个异步操作的中间代码 IL,包含了状态机状态切换的逻辑
1
2
3
4
5
6
7
8
9
10
11
12
13
// ... StateMachine.MoveNext() 部分代码
awaiter1.GetResult(); // 任务一完成
task2.Start(); // 任务二启动
awaiter2 = task2.GetAwaiter(); // 获取等待器
if (!awaiter2.IsCompleted)
{ // 未完成进入
this.<>1__state = num2 = 1;
this.<>u__1 = awaiter2;
Program.<<Main>$>d__0 stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, Program.<<Main>$>d__0>(ref awaiter2, ref stateMachine); // 配置 任务二完成后的行为(哪个状态机哪个任务,从而去执行状态转移函数, 就是这个 MoveNext)
return; // 直接返回,避免主线程阻塞。
}
// ...