Post

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;	// 直接返回,避免主线程阻塞。
}
// ...
This post is licensed under CC BY 4.0 by the author.