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,由实际应用场景决定。在并发执行多任务的场景,尽量在异步方法中少写耗时的同步方法,以最大发挥异步方法的优势。
Preview: