C# async/await编程模型的执行逻辑解析
简介
C# 5引入了async/await异步编程模型,旨在进一步简化异步编程的难度,让异步执行的代码看起来像同步代码一样简洁和容易理解。但初学者往往会将此异步编程模型与多线程编程模型混淆,以致引起一些不必要的错误如死锁或性能损失。
简而言之,Task是对异步操作的抽象,而非对线程的抽象。 可以认为await关键字暂时释放了线程控制权给界面或主线程,而待较为耗时的异步操作完成之后继续执行下面的代码。一般来说,此处所谓的释放控制权并不一定另起一个线程进行异步操作,这也是许多新接触async/await关键字的开发者最容易疑惑的地方。
在分析async代码执行顺序前,首先比较一下Thread.Sleep()和Task.Delay():
- Thread.Sleep(): 同步方法,阻塞当前线程,返回值void。
- Task.Delay(): 异步方法,awaitable,返回值Task。
因此,我们结合这两种方法模拟一个耗时的异步函数DoWorkAsync(),其中同步耗时操作主要发生在Thread.Sleep():
public static async Task DoWorkAsync(string name)
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Enter {name}");
Thread.Sleep(100);
await Task.Delay(500);
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Leave {name}");
}
“假”多任务异步执行
一种常见的异步操作场景即有多个任务希望并发执行,待所有任务执行完毕后,合并结果。则最为常见的写法如下:
class Program
{
static void Main(string[] args)
{
Test().Wait();
Console.ReadKey();
}
public static async Task Test()
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Main");
var task1 = DoWorkAsync("Task1");
var task2 = DoWorkAsync("Task2");
await task1;
await task2;
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Leave Main");
}
public static async Task DoWorkAsync(string name)
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Enter {name}");
Thread.Sleep(100);
await Task.Delay(500);
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Leave {name}");
}
}
执行结果:
[1] Main
[1] Enter Task1
[1] Enter Task2
[4] Leave Task1
[4] Leave Task2
[4] Leave Main
代码执行顺序如下图所示,括号中的数字代表执行步骤顺序:
即首先执行Task1在await关键字之前的同步操作,然后让出控制权到主线程,执行Task2在await关键字之前的同步操作,再让出控制权到主线程,等待两个Task执行完毕。因此,若每个Task在await关键字前有一些开销较大的同步操作,则在执行多个Task时会造成似乎它们在顺序同步执行的错觉。所以这里称其为“假”多任务异步执行。
同时注意到在执行Task1时的线程Id与主线程相同,说明每一个Task在执行行并非一定会创建单独线程,即Task只是对异步操作的抽象。
“真”多任务异步执行
如果希望程序像下图一样同时开始执行两个Task,可以使用Task.Run()指定一个Task在线程池中执行。
public static async Task Test()
{
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Main");
var task1 = Task.Run(() => DoWorkAsync("Task1"));
var task2 = Task.Run(() => DoWorkAsync("Task2"));
await task1;
await task2;
Console.WriteLine($"[{Thread.CurrentThread.ManagedThreadId}] Leave Main");
}
执行结果:
[1] Main
[4] Enter Task2
[3] Enter Task1
[5] Leave Task1
[6] Leave Task2
[6] Leave Main
注意到这里Task1与Task2所在的线程与主线程不同,且进入函数的时机不确定:可能出现先Task2后Task1。而在“假”异步多线程的情况,Task1一定先于Task2执行。
总结
- Task是对异步操作的抽象,而非对线程的抽象。
- 是否需要显示指定在线程池中执行Task,由实际应用场景决定。在并发执行多任务的场景,尽量在异步方法中少写耗时的同步方法,以最大发挥异步方法的优势。