- A+
受众
.Net c#开发人员
概念
免责声明:计算机科学很多概念都没有类似数学一样严谨的定义,以下概念为个人理解
程序(Program)
静态模板
进程(Process)
程序的实例,一个程序可以有多个进程
线程(Thread)
一个进程可以有多个线程,CPU调度分配最小单位
协程(Coroutine)
一个线程有多个协程,用户控制
同步(Synchronous)
执行一个任务只有完成后,才会执行下一个任务
异步(Asynchronous)
执行一个任务不管有没有完成,就开始执行下一个任务,如果任务没有结果则任务完成后会通过状态、通知和回调告知完成。目的是为了提高资源利用率,减少线程等待,而不是为了提高性能,有时使用异步反而性能会降低,但因为减少了等待所以单位时间内做的任务更多了,从而导致性能提升了,所以是用CPU资源换时间
-
状态:每隔一定时间轮询异步方法的状态,简单但是效率低
-
通知:异步方法完成后发出通知
-
回调:与通知类似,异步方法完成后会调用指定的回调函数
同步vs异步
区别在于任务之间是否会等待完成才开始执行,同步有明确的调用链顺序。异步可以类比计算机休眠(Sleep)然后再启动,同步可以类比计算机睡眠(Shut Down)
异步和多线程
多线程是实现异步的其中一种方式,但是异步本身不一定会创建线程,而是任务完成以后系统找寻同步上下文,如果存在则使用之前的同步上下文对应的线程继续执行后续语句,否则自动创建线程执行后续语句;单线程通过事件通知也能做到异步:某个线程去读取某个文件Bytes,CPU告诉磁盘去读取该文件,该线程就返回去做其它任务了,等磁盘通过DMA读取完成该文件完成后通过事件通知该线程,该线程会从内存中拿到文件的Bytes
阻塞(Blocking)
线程挂起 等待某个操作执行完成,期间CPU不会分配时间片给该线程,该线程无法执行其它代码
非阻塞(Non Blocking)
就算某个方法没执行完,线程也会返回
阻塞vs非阻塞
调用者是否一直等待结果,期间不执行其它代码
同步异步 vs 阻塞非阻塞
同步异步关注如何得到结果以及得到结果后如何告知调用者完成,阻塞非阻塞关注如何等待结果,是线程等着完成还是先做其它事情等完成的后通知、回调或者轮询状态。同步和异步与阻塞和非阻塞没有任何关系,所以可以存在4种情况:同步阻塞、同步非阻塞、异步阻塞和异步非阻塞。
同步阻塞
某个线程执行一个同步方法,方法返回结果前, 后续代码不会执行,且该线程一直挂起等待结果,期间不会复用执行其它代码
public static string SyncBlocking() { return SyncDownload();// 线程执行同步方法,并等待它完成期间不做其它任何事情 } public static string SyncDownload() { var webClient = new WebClient(); return webClient.DownloadString("https://www.baidu.com"); }
同步非阻塞
某个线程执行一个同步方法,方法没有得到结果前不会返回,该线程会回线程池,期间可能被复用执行其它代码
public static async Task<string> SyncNonBlocking() { // 此处会从线程池中选择一个新线程B执行SyncMethod,如果要具体控制使用哪个线程或者如何排队需要使用Task.Factory.StartNew var task = Task.Run(() => SyncDownload()); var result = await task;// 原线程A执行到此然后马上返回,不等待同步方法完成就返回,这样可以去做其它事情 return result; } public static string SyncDownload() { var webClient = new WebClient(); return webClient.DownloadString("https://www.baidu.com"); }
异步阻塞
某个线程执行一个异步方法,方法立即返回,后续代码会被执行,直至执行到需要返回结果的代码,该线程挂起等待结果,期间不会复用执行其它代码
public static string AsyncBlocking() { var task = AsyncDownload(); return task.Result; //虽然是异步操作,但是线程呆呆的等它完成然后获取结果,期间不做其它事情 } public static Task<string> AsyncDownload() { var webClient = new WebClient(); return webClient.DownloadStringTaskAsync("https://www.baidu.com"); }
异步非阻塞
某个线程执行一个异步方法,方法立即返回,后续代码会被执行,直至执行到需要返回结果的代码,该线程会回线程池,期间可能被复用执行其它代码
public static async Task<string> AsyncNonBlocking() { var result = await AsyncDownload();// 线程执行到此然后马上返回,可以去做其它事情 return result;//完成后会获取结果并继续执行后面的代码 } public static Task<string> AsyncDownload() { var webClient = new WebClient(); return webClient.DownloadStringTaskAsync("https://www.baidu.com"); }
串行
系统顺序执行多个任务,前一个任务完成后才开始执行下一个任务。同一时刻,只可以有一个任务是开始状态或执行状态
例子:1个处理窗口 1个任务队伍 任务处理完成 移除
时间片1 | 时间片2 | 时间片3 | 时间片4 | 时间片5 |
---|---|---|---|---|
?? | ?? | ??? | ? | ???? |
并发(Concurrency)
系统具有时间段的同时交替处理多个任务的能力,前一个任务可能没完成也会开始执行后面的任务。同一时刻,可以有多个任务是开始状态,但只有1个是执行状态。可以类比一个人能同时做多件事但是每个时刻只在做某一件事。
单核CPU通过时间分片给不同线程(可以是不同进程的线程)达到并发
某个CPU核心切换执行不同线程的指令时会存在保存和恢复当前环境(CPU执行到哪个指令了对应寄存器和栈是什么值,线程的上下文是什么)的线程上下文切换的开销
例子:1个处理窗口 N个任务队伍 如果任务处理完成则移除 如果任务处理一半完成则重新排队
时间片1 | 时间片2 | 时间片3 | 时间片4 | 时间片5 |
---|---|---|---|---|
?? | ?? | |||
??? | ? | |||
???? |
并行(Parallelism)
系统具有时间点同时执行多个任务的能力。同一时刻,有多个任务是开始状态,且有2个以上任务是执行状态。可以类比多个人做多个事
例子:N个处理窗口 N个队伍
时间片1 | 时间片2 |
---|---|
?? | ?? |
??? | ? |
???? |
并发vs并行
并发有线程切换,并行没有。可以既并发又并行(多核CPU,每个CPU在做多个任务),也可以不并发也不并行(单核CPU,串行执行任务),还可以只并发(单核CPU,多个任务切换执行)或者只并行(多核CPU,每个CPU只执行某一个任务),所以两者没有关系,只是相似概念
为什么需要异步编程?
为了解决UI线程阻塞导致无响应,用户界面体验差,以及各种IO(文件、网络请求和数据库sql等)线程阻塞导致线程资源利用率差。线程阻塞时不会使用CPU资源,但每个线程本身会占用1M左右的内存资源,如果数量多会耗费很多内存资源,而内存紧张时会频繁GC回收内存,GC回收内存时又会暂停线程并耗费CPU资源,这部分GC时间如果省出来就能让CPU做更多的业务工作,如果物理内存资源紧张还会导致磁盘上的虚拟内存与物理内存出现频繁的内存页Swap交换,这个磁盘读写操作比内存读写操作慢的多,其次线程即使阻塞,调度程序还是会来回切换以检查是否可以恢复执行,使用多线程解决问题会导致大量频繁的上下文切换进而浪费大量CPU时间,从而导致负载变高,延迟增加,吞吐量减少,从而拖慢整个系统性能。异步编程能充分利用线程资源避免浪费,线程在执行长时任务时能马上释放线程资源,这样该线程能被回收到线程池然后继续执行其它任务,如果是UI线程则能继续响应用户其它操作,从而达到相同的线程数能处理更多的请求或者说相同数量的请求只需要更少的线程资源来处理
何时使用异步编程?
IO密集型
网络请求数据、访问数据库或读写文件系统,这一类操作CPU不会直接参与,而是需要等待某些事情完成。这一类要使用asyncawait但不要使用Task.Run,而是通常使用TaskCompletionSource来实现通知IO操作已完成
CPU密集型
不包含各种IO操作,如游戏计算伤害,计算π,计算素数,这一类要使用asyncawaitTask.Run还可以考虑使用TPL
为什么推荐使用TAP而不是其它方式实现异步?
因为TAP使用asyncawait关键字与Task和Task
.Net异步编程历史
EAP(Event-based Asynchronous Pattern) 基于事件的异步模式
方法名以Async并有一个或多个相关事件,该方式不同任务使用同一个实例,会导致执行相同的事件处理函数,而且需要编写两个方法而不是一个方法
private void DumpWebPage(string url) { WebClient webClient = new WebClient(); webClient.DownloadStringCompleted += OnDownloadStringCompleted; webClient.DownloadStringAsync(uri); } private void OnDownloadStringCompleted(object sender, DownloadStringCompletedEventArgs eventArgs) { Console.WriteLine(eventArgs.Result); }
如果某个方法只有EAP基于事件的异步模式的API,那么可以通过以下方法封装为TAP基于任务的异步模式
public static Task<string> DownloadStringAsync(Uri url) { var tcs = new TaskCompletionSource<string>(); var webClient = new WebClient(); webClient.DownloadStringCompleted += (s,e) => { if (e.Error != null) { tcs.TrySetException(e.Error); } else if (e.Cancelled) { tcs.TrySetCanceled(); } else { tcs.TrySetResult(e.Result); } }; webClient.DownloadStringAsync(url); return tcs.Task; }
APM(Asynchronous Programming Model) 异步编程模型
该方式能避免EAP的问题,提供BeginEnd开头的成对方法,需要强制传递一个object参数以便传递状态,用的object类型而不是强类型,其次还需要调用EndXXX(asyncResult),而且还需要编写两个方法而不是一个方法
private void LookupHostName() { object unrelatedObject = "hello"; Dns.BeginGetHostAddresses("oreilly.com", OnHostNameResolved, unrelatedObject); } private void OnHostNameResolved(IAsyncResult ar) { object unrelatedObject = ar.AsyncState; IPAddress[] addresses = Dns.EndGetHostAddresses(ar); // Do something with addresses ... }
如果某个方法只有APM异步编程模型的API,那么可以通过以下方法封装为TAP基于任务的异步模式
Task t = Task<IPHostEntry>.Factory.FromAsync<string>(Dns.BeginGetHostEntry, Dns.EndGetHostEntry, hostNameOrAddress, null); // 与以下代码等效 var tcs = new TaskCompletionSource<int>(); Dns.BeginGetHostEntry(hostNameOrAddress, iar => { try { tcs.TrySetResult(Dns.EndRead(iar)); } catch(OperationCanceledException) { tcs.TrySetCanceled(); } catch(Exception exc) { tcs.TrySetException(exc); } }, null); return tcs.Task;
TAP(Task-based Asynchronous Pattern) 基于任务的异步模式
提供一个以Async结尾通常返回Task、Task
只用Task不使用async await的异步方式
private Task<string> DumpWebPageAsync(string url) { WebClient webClient = new WebClient(); var task = webClient.DownloadStringTaskAsync(url); // ContinueWith作用是指定task完成以后继续执行的操作 task.ContinueWith(page => { Console.WriteLine(page); }); }
ContinueWith方式如果碰到循环调用就只能使用递归,递归调用不易于理解和调试,所以不推荐使用,比如如下代码
private void LookupHostNames(string[] hostNames) { LookUpHostNamesHelper(hostNames, 0); } private static void LookUpHostNamesHelper(string[] hostNames, int i) { Task<IPAddress[]> ipAddressesPromise = Dns.GetHostAddressesAsync(hostNames[i]); ipAddressesPromise.ContinueWith(_ => { IPAddress[] ipAddresses = ipAddressesPromise.Result; // Do something with address ... if (i + 1 < hostNames.Length) { LookUpHostNamesHelper(hostNames, i + 1); } }); }
使用Task以及async await的异步方式,该方式最接近原始同步代码,但是效果却是异步的
private async Task<string> DumpWebPageAsync(string url) { WebClient webClient = new WebClient(); var page = await webClient.DownloadStringTaskAsync(url); Console.WriteLine(page); } // 等价于 private async Task<string> DumpWebPageAsync(string url) { WebClient webClient = new WebClient(); var task = webClient.DownloadStringTaskAsync(url) var page = await task; Console.WriteLine(page); }
Task和Task
如果在一个Task或Task
我们还可以顺序等待多个Task或Task
WebClient webClient1 = new WebClient(); WebClient webClient2 = new WebClient(); // 该行运行完以后,到达下一行代码时,firstTask的Result结果可能还不可用 Task<string> firstTask = webClient1.DownloadStringTaskAsync("https://baidu.com"); Task<string> secondTask = webClient2.DownloadStringTaskAsync("https://google.com"); // 如果firstTask抛出异常 会导致await secondTask无法执行 string firstPage = await firstTask; string secondPage = await secondTask;
也可以等待全部完成或者任一任务完成
WebClient webClient1 = new WebClient(); WebClient webClient2 = new WebClient(); Task<string> firstTask = webClient1.DownloadStringTaskAsync("https://baidu.com"); Task<string> secondTask = webClient2.DownloadStringTaskAsync("https://google.com"); // 或者用 await Task.WhenAny(firstTask, secondTask); await Task.WhenAll(firstTask, secondTask); // 因为上面语句使用了Task.WhenAll 所以firstTask肯定是完成状态,下面的await将会立即获得结果,即以同步形式在同一线程中获得结果 string firstPage = await firstTask; string secondPage = await secondTask;
也可以使用lambda接收回调函数的方式来实现异步,但是如果嵌套的回调会导致代码难以阅读理解,且异常不会抛出回调用者,导致异常无法处理
什么是Task和Task,两者有什么区别?
Task表示一类长时间的任务,在未来某个时刻会结束。它是针对任务的抽象,不是线程。可以调用Task或Task
在.Net中有Task和Task
- Task类型没有返回值类似void
- Task
类型有强类型返回值T
使用asyncawait关键字有什么作用
只在方法签名中加入async关键字,代码执行效果没有任何变化,只是让这样的方法中能使用await关键字
在某个方法调用前加入await关键字的效果如下:
如果Task或Task
编译器重写
async 和 await关键字会影响编译器对方法的重写,重写后代码不包含async和await关键字
重写后的代码如果任务都已完成那么执行效果就和同步方法一个效果
async关键字转化
一个方法标记了async关键字,该方法就被允许在内部使用await关键词,所以virtual虚方法中使用async关键字override重写方法可以不使用async关键字。async关键字会影响方法生成状态机骨架代码
class BaseClass { public virtual async Task<int> SomeMethod() { ... } } class SubClass : BaseClass { // This overrides SomeMethod above public override Task<int> SomeMethod() { ... } }
以下为只要使用了async关键字就会生成的状态机骨架代码,只有async关键字的方法执行效果和同步方法没有区别,只是结果可能被封装在Task中,且会多执行一些代码,所以相比于对应的同步方法会更慢
// 异步方法所在类 public class XXXClass { // 原始只含有async关键字的异步方法 public async Task AAsync() { //空方法体 } } //以下都是状态机骨架代码,编译器生成的名字使用了只允许编译器使用的尖括号,而该符号不利于阅读所以进行了重命名 public Task AAsync() { //创建状态机 并初始化,使用了async关键字就会生成以下代码,不管内部是否使用await关键字 GeneratedAsyncStateMachine generatedAsyncStateMachine = new GeneratedAsyncStateMachine { referenceThis = this, builder = AsyncVoidMethodBuilder.Create(), //-1为初始状态 state = -1 }; //使用当前线程执行IAsyncStateMachine.MoveNext()方法 generatedAsyncStateMachine.builder.Start(ref generatedAsyncStateMachine); return generatedAsyncStateMachine.builder.Task; } // 编译器生成的状态机类,会根据状态从相应位置执行代码,状态的划分详见await关键字转化 [CompilerGenerated] private sealed class GeneratedAsyncStateMachine : IAsyncStateMachine { // 状态,用来保存当前代码执行到那个位置了。-1为初始状态,会从开头开始执行;-2为结束状态 public int state; // 状态机控制逻辑,指示Task是否完成,作用类似TaskCompletionSource,但是优化过比如使用struct结构而不是class类 // 如果是async void 会使用AsyncVoidMethodBuilder结构 // 如果是async Task<T>会使用AsyncTaskMethodBuilder<T>结构 public AsyncTaskMethodBuilder builder; // 此处如果异步方法内有局部变量,还会包含声明在此处 // 非静态异步方法所在类,如果是静态方法将没有该变量。用来访问异步方法所在类的变量 public XXXClass referenceThis; void IAsyncStateMachine.MoveNext() { int num = this.state; try { // 以下会用原始代码方法体替换,如果使用await关键字,还会有对应状态转化,详见下面await关键字转化 } catch (Exception exception) { //-2为结束状态 this.state = -2; //设置Task.Status为Faulted 并在调用Task.GetResult()或访问Task.Result属性时抛出该异常 等价于 throw; this.builder.SetException(exception); } } // 对应IAsyncStateMachine接口的空实现 [DebuggerHidden] void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { } }
await关键字转化
只有方法使用了async关键字才能使用await关键字,编译器会为await关键字所在行的代码作为分割点,依次编号为-1、0 、1、2...状态,以便异步执行完成后继续恢复该方法执行时能找到对应位置继续执行
await XXXAsync(); // 转化为MoveNext方法中的case x那段 [CompilerGenerated] private sealed class GeneratedAsyncStateMachine : IAsyncStateMachine { TaskAwaiter taskAwaiterField; void IAsyncStateMachine.MoveNext() { try { TaskAwaiter taskAwaiter; switch(state) { case x: taskAwaiter=XXXAsync().GetAwaiter(); if(!taskAwaiter.IsCompleted) { state = x+1; // taskAwaiter为局部变量,方法返回后值会消失,所以需要保存到类中的字段中 taskAwaiterField = taskAwaiter; builder.AwaitUnsafeOnCompleted(ref awaiter, ref generatedAsyncStateMachine); return; } case x+1: taskAwaiter.GetResult(); // 下一个状态的其它代码 ... } catch (Exception exception) { state = -2; //AsyncVoidMethodBuilder SetException 会抛出该异常 等价于 throw; builder.SetException(exception); return; } } }
一个异步方法调用前加了await关键字,该异步方法如果未完成,注册Task完成后回调本方法以便执行后续方法调用并且该线程立即返回并不会执行后面的语句包括return语句,Task完成后会触发事件,回调方法会根据同步上下文的线程继续进行调用后续方法,如果同步上下文为空则从线程池中选择一个线程继续执行后续方法
一个异步方法调用前如果未加await关键字,该异步方法如果未完成则线程立即返回,后续方法调用都不会等待其完成马上被当前线程继续进行调用
可以对比以下程序执行结果差异理解
//直接使用 await static async Task Main(string[] args) { await Task.Delay(2000); Console.WriteLine("Download file1"); await Task.Delay(2000); Console.WriteLine("Download file2"); Console.WriteLine("Start data input, enter your name:"); string str = Console.ReadLine(); Console.WriteLine(str); Console.Read(); // 结果 // Download file1 // Download file2 // Start data input, enter your name: // luca // luca } // 包装为方法以后使用await // 此例也说明,提取包含await的语句为方法,需要在方法前添加await才能与原代码保持一致 static async Task Main(string[] args) { Method1(); // 改为 await Method1(); 才能与原代码执行逻辑保持一致 Method2(); // 改为 await Method2(); 才能与原代码执行逻辑保持一致 Console.WriteLine("Start data input, enter your name:"); string str = Console.ReadLine(); Console.WriteLine(str); Console.Read(); // 结果以下另种情况随机 // Start data input, enter your name: // luca // luca // Download file2 // Download file1 // Start data input, enter your name: // luca // luca // Download file1 // Download file2 } static async Task Method1() { await Task.Delay(2000); Console.WriteLine("Download file1"); } static async Task Method2() { await Task.Delay(2000); Console.WriteLine("Download file2"); }
下面举例说明编译器如何对标记async和await关键字的异步方法进行重写
async void
这种返回类型表示fire and forget也就是触发后就遗忘不管它是否执行完成,也不管它的结果以及是否有异常了
重写前:
class AsyncAwaitClass { public async void CallAsync() { Method1Sync();//代表上一个await和这个await之间的所有语句 await MethodAsync();//对于第X条await语句 X从0开始 Method2Sync();//代表这个await和下一个await之间的所有语句 } void Method1Sync() { } void Method2Sync() { } Task MethodAsync() => Task.CompletedTask; }
重写后:
class AsyncAwaitClass { //原方法会被骨架方法替换 //骨架方法 就相当于直接调用 IAsyncStateMachine.MoveNext() public void CallAsync() { //创建状态机 并初始化,使用了async关键字就会生成以下代码,不管内部是否使用await关键字 GeneratedAsyncStateMachine generatedAsyncStateMachine = new GeneratedAsyncStateMachine { referenceThis = this, builder = AsyncVoidMethodBuilder.Create(), state = -1 }; //使用当前线程执行IAsyncStateMachine.MoveNext()方法 generatedAsyncStateMachine.builder.Start(ref generatedAsyncStateMachine); } private void Method1Sync() { } private void Method2Sync() { } private Task MethodAsync() => Task.CompletedTask; //编译器生成的异步状态机类 private sealed class GeneratedAsyncStateMachine : IAsyncStateMachine { //状态 -1为初始状态 -2为结束状态 public int state; public AsyncVoidMethodBuilder builder; public AsyncAwaitClass referenceThis; // 用来保存上次执行到的await所在的Task的Awaiter,以便恢复后继续执行时恢复该变量值。TaskAwaiter是用来注册Task完成后的回调函数的 private TaskAwaiter taskAwaiter; void IAsyncStateMachine.MoveNext() { try { TaskAwaiter awaiter; switch(state) { default: goto Label0; case 0: awaiter = taskAwaiter; taskAwaiter = default; state = -1; goto Label1; } Label0: referenceThis.Method1Sync(); awaiter = referenceThis.MethodAsync().GetAwaiter(); if (!awaiter.IsCompleted) { state = 0; taskAwaiter = awaiter; var generatedAsyncStateMachine = this; builder.AwaitUnsafeOnCompleted(ref awaiter, ref generatedAsyncStateMachine); return; } Label1: awaiter.GetResult(); referenceThis.Method2Sync(); } catch (Exception exception) { state = -2; //AsyncVoidMethodBuilder SetException 会抛出该异常 等价于 throw; builder.SetException(exception); return; } state = -2; builder.SetResult(); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { } } }
因为MethodAsync返回的是Task.CompletedTask 它的IsCompleted属性默认值是true所以没有状态切换和回调,也就等价于
class AsyncAwaitClass { public void CallAsync() { try { Method1Sync(); MethodAsync().GetAwaiter().GetResult(); Method2Sync(); } catch { throw; } } }
如果MethodAsync返回的Task IsCompleted是false 那么就会有状态切换和回调 就会执行IAsyncStateMachine.MoveNext() 两次
第一次执行:
Method1Sync(); awaiter = MethodAsync().GetAwaiter(); //改变状态 以便下次回调 调用恢复awaiter的语句 state = 0; //将awaiter保存到字段中 以便下次回调恢复 taskAwaiter = awaiter;
第二次执行:
//从字段中恢复awaiter awaiter = taskAwaiter; //获取结果 因为回调的时候 task已经完成 所以TaskAwaiter.GetResult()不会等待 直接返回结果 taskAwaiter.GetResult(); Method2Sync();
可以看出如果Task已完成(IsCompleted为True) 那么一定不会有线程的切换 代码顺序执行 没有回调 这样能避免重复await某个已完成的Task导致线程切换 额外消耗资源
最佳实践: async void 只在event handler里使用
最佳实践: async void 方法 如果内部调用的方法抛出异常 该方法也会将异常抛出 导致未处理的异常引起系统崩溃 所以需要在方法内部加try catch处理可能抛出来的异常
class AsyncAwaitClass { public async void CallAsync() { try { Method1Sync(); await MethodAsync(); Method2Sync(); } catch(XXXException exception) { //处理异常代码 ... } //其它异常处理代码 ... } }
async Task
相对于async void,返回async Task能传递异常,又因为返回的是Task包含是否完成状态,所以可以在这类方法前使用await等待它完成
重写前:
class AsyncAwaitClass { async Task CallAsync() { Method1Sync();//代表上一个await和这个await之间的所有语句 await MethodAsync();//对于第X条await语句 X从0开始 Method2Sync();//代表这个await和下一个await之间的所有语句 } }
重写后:
class AsyncAwaitClass { //原方法会被骨架方法替换 public Task CallAsync() { //创建状态机 并初始化 GeneratedAsyncStateMachine generatedAsyncStateMachine = new GeneratedAsyncStateMachine { referenceThis = this, //因为返回类型是Task 所以builder是AsyncTaskMethodBuilder类型 而不是AsyncVoidMethodBuilder builder = AsyncTaskMethodBuilder.Create(), state = -1 }; //使用当前线程执行IAsyncStateMachine.MoveNext()方法 generatedAsyncStateMachine.builder.Start(ref generatedAsyncStateMachine); //因为builder是AsyncTaskMethodBuilder类型 所以可以返回它的Task属性 用来指示状态机方法是否全部完成或取消或异常 return generatedAsyncStateMachine.builder.Task; } private void Method1Sync() { } private void Method2Sync() { } private Task MethodAsync() => Task.CompletedTask; //编译器生成的异步状态机类 private sealed class GeneratedAsyncStateMachine : IAsyncStateMachine { public int state; public AsyncTaskMethodBuilder builder; public AsyncAwaitClass referenceThis; private TaskAwaiter lastAwaiter; void IAsyncStateMachine.MoveNext() { try { TaskAwaiter awaiter; //此处switch是为了回调时能获取上一次的awaiter switch (state) { default: goto Label0; case 0: //以下三行代码 对于第0条await关键字标记的方法不会生成 也就是上方default // 回调时会执行 用于恢复上次执行的异步方法的Awaiter awaiter = lastAwaiter; taskAwaiter = default; state = -1; goto Label1; } Label0: referenceThis.Method1Sync(); awaiter = referenceThis.MethodAsync().GetAwaiter(); //如果异步方法未完成 if (!awaiter.IsCompleted) { //设置 状态+1 state = 0; lastAwaiter = awaiter; var generatedAsyncStateMachine = this; //设置同步上下文 和 回调到本IAsyncStateMachine.MoveNext函数 builder.AwaitUnsafeOnCompleted(ref awaiter, ref generatedAsyncStateMachine); //直接返回 并不会执行后续GetResult()方法 而是通过回调 跳转本函数 判断state 恢复对应awaiter 并继续执行后续代码 return; } Label1: //上一条await关键字标记的方法 生成的TaskAwaiter.GetResult() //如果是Task<TResult> 则会生成 var result=TaskAwaiter<TResult>.GetResult() awaiter.GetResult(); referenceThis.Method2Sync(); } catch (Exception exception) { state = -2; builder.SetException(exception); return; } state = -2; builder.SetResult(); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { } } }
从上面的重写可以看出:
如果异步方法调用后马上已完成(比如调用的异步函数是直接返回Task.FromResult(xxx) 那么IsCompleted属性值默认就是true) 那么代码会同步执行,跳过if (!awaiter.IsCompleted)段和switch case段的代码 等价于
void IAsyncStateMachine.MoveNext() { Method1Sync(); MethodAsync().GetAwaiter().GetResult(); Method2Sync(); }
async Task
相对于async Task,async Task
重写前:
public class AsyncAwaitClass { public async Task<string> CallAsync() { MethodSync1(); var result = await MethodAsync(); MethodSync2(); return result; } private void MethodSync1() { } private void MethodSync2() { } private async Task<string> MethodAsync() { await Task.Delay(1000); return "Hello World"; } }
重写后:
public class AsyncAwaitClass { public Task<string> CallAsync() { //构造状态机并初始化 MethodAsyncGeneratedAsyncStateMachine methodAsyncdGeneratedAsyncStateMachine = new MethodAsyncGeneratedAsyncStateMachine { referenceThis = this, builder = AsyncTaskMethodBuilder<string>.Create(), state = -1 }; //执行第一个MoveNext methodAsyncdGeneratedAsyncStateMachine.builder.Start(ref methodAsyncdGeneratedAsyncStateMachine); return methodAsyncdGeneratedAsyncStateMachine.builder.Task; } private void MethodSync1() { } private void MethodSync2() { } private Task<string> MethodAsync() { MethodAsync3GeneratedAsyncStateMachine methodAsyncGeneratedAsyncStateMachine = new MethodAsync3GeneratedAsyncStateMachine { referenceThis = this, builder = AsyncTaskMethodBuilder<string>.Create(), state = -1 }; methodAsyncGeneratedAsyncStateMachine.builder.Start(ref methodAsyncGeneratedAsyncStateMachine); return methodAsyncGeneratedAsyncStateMachine.builder.Task; } private sealed class MethodAsyncGeneratedAsyncStateMachine : IAsyncStateMachine { void IAsyncStateMachine.MoveNext() { string result; try { TaskAwaiter<string> awaiter; switch (state) { //-1 表示开始 default: goto Label0; case 2: awaiter = methodAsyncAwaiter; methodAsyncAwaiter = default; state = -1; goto Label3; } //从开始到第一个await间的代码 Label0: referenceThis.MethodSync1(); awaiter = referenceThis.MethodAsync().GetAwaiter(); if (!awaiter.IsCompleted) { state = 2; methodAsyncAwaiter = awaiter; var methodAsyncGeneratedAsyncStateMachine = this; builder.AwaitUnsafeOnCompleted(ref awaiter, ref methodAsyncGeneratedAsyncStateMachine); return; } //结束代码 用于获取第三个异步方法结果 Label3: methodAsyncResult = awaiter.GetResult(); thisMethodResult = methodAsyncResult; methodAsyncResult = null; referenceThis.MethodSync2(); result = thisMethodResult; } catch (Exception exception) { // -2 表示结束 state = -2; thisMethodResult = null; //如果该异步方法返回类型是Task 或 Task<TResult> 那么 SetException 会将exception捕获设置Task内部属性且会设置状态为TASK_STATE_FAULTED 导致Task.IsFault为true //如果有异常那么在执行Task<T>.Wait() Task.Result(AggregateException包裹的异常) 和 Task.GetAwaiter().GetResult()方法时 会抛出对应原始异常 builder.SetException(exception); return; } // -2 表示结束 state = -2; thisMethodResult = null; //设置build.Task的Result属性为result变量值 builder.SetResult(result); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { } public int state; public AsyncTaskMethodBuilder<string> builder; public AsyncAwaitClass referenceThis; private TaskAwaiter<string> methodAsyncAwaiter; private string thisMethodResult; private string methodAsyncResult; } private sealed class MethodAsyncGeneratedAsyncStateMachine : IAsyncStateMachine { void IAsyncStateMachine.MoveNext() { int num = state; string result; try { TaskAwaiter awaiter; if (num != 0) { awaiter = Task.Delay(1000).GetAwaiter(); if (!awaiter.IsCompleted) { state = 0; taskDelayAwaiter = awaiter; var methodAsyncGeneratedAsyncStateMachine = this; builder.AwaitUnsafeOnCompleted(ref awaiter, ref methodAsyncGeneratedAsyncStateMachine); return; } } else { awaiter = taskDelayAwaiter; taskDelayAwaiter = default; state = -1; } awaiter.GetResult(); result = "Hello World"; } catch (Exception exception) { state = -2; builder.SetException(exception); return; } state = -2; builder.SetResult(result); } void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) { } public int state; public AsyncTaskMethodBuilder<string> builder; public AsyncAwaitClass referenceThis; private TaskAwaiter taskDelayAwaiter; } }
综合
综上所述
async await 关键字标记的方法 会进行如下重写
重写前:
//以下为伪代码 不能编译运行 class AsyncAwaitClass { //async void 或 async Task 或 async Task<TResult> public async void|Task|Task<TResult> CallAsync(XXX xxx,YYY yyy,ZZZ zzz,...) { try { Method1(); await Method1Async(); Method2(); if(Condition1) { Method3(); await Method2Async(); Method4(); } while(condition2) { Method5(); await Method3Async(); Method6(); } } catch { Method7(); await Method4Async(); Method8(); } finally { Method9(); await Method5Async(); Method10(); } } }
重写后:
class AsyncAwaitClass { //原方法会被骨架方法替换 Task<TResult> 或 Task 或 void CallAsync() { //创建状态机 并初始化 GeneratedAsyncStateMachine generatedAsyncStateMachine = new GeneratedAsyncStateMachine(); generatedAsyncStateMachine.referenceThis = this; //因为返回类型是Task 所以builder是AsyncTaskMethodBuilder类型 而不是AsyncVoidMethodBuilder generatedAsyncStateMachine.builder = AsyncTaskMethodBuilder.Create(); generatedAsyncStateMachine.state = -1; //使用当前线程执行IAsyncStateMachine.MoveNext()方法 generatedAsyncStateMachine.builder.Start<GeneratedAsyncStateMachine>(ref generatedAsyncStateMachine); //因为builder是AsyncTaskMethodBuilder类型 所以可以返回它的Task属性 用来指示状态机方法是否全部完成或取消或异常 return methodAsyncdGeneratedAsyncStateMachine.builder.Task; } //编译器生成的异步状态机类 private sealed class GeneratedAsyncStateMachine : IAsyncStateMachine { void IAsyncStateMachine.MoveNext() { ... switch(state) { ... case X-1: //以下三行代码 对于第0条await关键字标记的方法不会生成 taskAwaiterX+1 = methodAsyncAwaiter;// 回调时会执行 用于恢复上次执行的异步方法的Awaiter methodAsyncAwaiter = default; state = -1; goto LabelX; ... } ... LabelX: //上一条await关键字标记的方法 生成的TaskAwaiter.GetResult() //如果是Task<TResult> 则会生成 var result=TaskAwaiter<TResult>.GetResult() taskAwaiterX.GetResult(); Method1Sync(); taskAwaiterX+1 = MethodAsync().GetAwaiter(); //如果异步方法未完成 if (!awaiter.IsCompleted) { //设置 状态+1 state = X; methodAsyncAwaiter = awaiter; var methodAsyncGeneratedAsyncStateMachine = this; //设置同步上下文 和 回调到本IAsyncStateMachine.MoveNext函数 builder.AwaitUnsafeOnCompleted(ref awaiter, ref methodAsyncGeneratedAsyncStateMachine); //直接返回 并不会执行后续GetResult()方法 而是通过回调 跳转本函数 判断state 恢复对应awaiter 并继续执行后续代码 return; } LabelX+1: taskAwaiterX+1.GetResult(); Method2Sync(); ... } } }
todo:async void 但是没有await语句
执行过程
-
await关键字之前的代码一定在原线程(调用该异步方法的方法所在线程)中执行
-
await task的task也是在当前线程执行,执行完成之后回调同步上下文如果为空则使用线程池中线程继续执行后续代码
-
如果Task.IsCompleted为true则表明task已经完成,那await task将会以同步的方式运行。以下为几种task已完成的情况
// 使用Task.FromResult创建的task是已完成的 public async Task<int> XXXAsync() { Task.FromResult(1); } // 有async关键字 但是方法内没有await关键字 public async Task XXXAsync() { } // 有async关键字 方法也有await关键字 但是task已完成 public async Task<int> XXXAsync() { // 但是该task已完成 await task; }
-
如果task状态为RanToCompletion,则表明任务运行完成无异常。task.GetResult()会获取返回结果
-
如果发生异常,会设置task中的exception属性,并修改状态为Faulted,并终止该task运行,调用回调方法执行跳至task.GetResult()处,GetResult()中会检测到状态为Faulted从而重新抛出该异常。如果包含多个异常则只会传播其中一个,但是AggregateException中包含所有异常
-
如果任务取消,则会抛出OperationCanceledException操作取消异常,并修改状态为Canceled,并终止该task运行,调用回调方法执行跳至task.GetResult()处,GetResult()中会检测到状态为Canceled从而重新抛出该异常
从执行过程可以看出可能出现一路执行下来并没有执行真正的异步操作--比如异步IO例如网络和文件读写(异步IO其实也是在一个IO completion port thread线程中执行的,只是由操作系统管理且全局唯一)或者新线程后台执行某个方法,全部都是同步执行,比如以下代码
async Task AAsync() { await BAsync(); } async Task BAsync() { await CAsync(); } async Task CAsync() { await Task.Complete; }
思考题
- 同一个方法使用async await方法一定比不使用的快 或者 慢?
- 为什么下列代码catch块无法捕获到异常,如何修改才能让控制台输出错误信息?
public Task AAsync() { try { BAsync(); } catch(Exception ex) { Console.WriteLine(ex); } } public Task BAsync() { throw new Exception(); }
- 为什么下列代码的Finally块无法执行,如何修改才能让控制台输出信息
static async void AlexsMethod() { try { await DelayForever(); } finally { // Never happens Console.WriteLine("Finally"); } } static Task DelayForever() { return new TaskCompletionSource<object>().Task; }
- 某异常抛出,Call Stack调用堆栈中只有XXX.
d_ZZZ.MoveNext()方法,请问你能据此找到对应的抛错方法吗? - 为什么有的Task需要调用Task.Start()才会运行,有的不需要?
答:通过Task构造函数创建的Task为冷任务需要调用Task.Start()启动,其它方式都为热任务,创建即会启动。对已经启动的Task调用Task.Start()会抛出InvalidOperationException无效操作的异常。
最佳实践
-
异步方法返回类型应该为Task、Task
等含有GetAwaiter()方法的类 -
使用async await 替换Task.ContinueWith,因为ContinueWith不会捕获同步上下文,导致诸如WPF WinForms之类的UI程序可能出现无法更新控件属性的异常(只有UI线程才能操作控件)
-
如果文件或网络或数据库等相关IO函数有异步方法版本都优先使用异步方法,这样能充分利用线程,提高性能增加吞吐量。反之如果编写IO密集型方法应该只提供异步方法
Thread.Sleep(); dbContext.SaveChanges(); fileStream.Read(); // 改为 await Task.Delay(3000); await dbContext.SaveChangesAsync(); await fileStream.ReadAsync();
- 异步方法如果不是触发后不管(fire and forget)这种用途那么最终都加await关键字
XXXAsync(); // 改为 await XXXAsync();
-
不要使用async void除非是event handler事件处理程序
-
async void方法要加try catch,否则方法内的异常不会被外部try catch捕获,该异常会以Task未观察异常抛出。原因是返回的是void而不是Task导致对应的Exception无法通过Task传递过来
-
await关键字后的异步方法都加上ConfigureAwait(false),从而避免死锁,也会更快,除非是Asp.NetWPF WinForms需要使用相同UI线程更新控件属性才不加上ConfigureAwait(false)或者加上ConfigureAwait(true)(该语句为默认值 属于冗余 不建议使用 最好删除),用来保证await前和await后语句为相同线程及对应同步上下文
await XXXAsync(); // 改为 await XXXAsync().ConfigureAwait(false);
- 同步方法中调用异步方法,为了避免死锁(只有WPF的UI线程、WinForms的UI线程和Asp.Net线程这类同步上下文不为空才会死锁)请使用Task.Run封装。这种会浪费一个线程仅仅是为了避免死锁,所以推荐使用ConfigureAwait(false)方式
private void Button_Click(object sender, EventArgs e) { label.Text = "waiting...."; Task<string> someTask = Task.Run(async () => { string msg =await GenerateCodeAsync(); return msg; }); label.Text += someTask.Result;// someTask.Wait() or someTask.GetAwaiter().GetResult() } private Task<string> GenerateCodeAsync() { return Task.Run<string>(() => GenerateCode()); } private string GenerateCode() { Thread.Sleep(2000); return "I'm back" ; }
-
长时间运行的任务不要使用Task.Run,因为这样使用的任务都是排队在线程池中执行,这样会导致后面的任务堆积,而且不能及时执行,应该使用Task.Factory.StartNew(() => {}, TaskCreationOptions.LongRunning);来新启动一个线程执行
-
不要调用异步方法后直接调用Wait()、GetAwaiter()和Result阻塞线程
public async Task AAAAsync() { xxx.YYYAsync().Wait(); xxx.YYYAsync().GetAwaiter().GetResult(); xxx.YYYAsync().Result; }
-
使用await Task.Delay(xxx)替代Thread.Sleep(xxx),后者会阻塞线程
-
异步方法(粗略可以认为就是使用了async关键字的方法)名使用Async结尾,非异步方法名不要以Async结尾
-
如果async方法没有await那么需要删掉async关键字,因为该方法会以同步方式运行而状态机代码没有起到应有作用反而增加了执行的语句数
-
如果async方法内只有一个await且在方法最后,那么就删掉async 关键字,await关键字改为return
public async Task XXXAsync() { await YYYAsync(); } //改为 public Task XXXAsync() { return YYYAsync(); }
- 不要在ActionAction
中使用async await, 因为这样会隐式的使用 async void
customers.ForEach(async c => await SendEmailAsync(c)); // 与以下代码等价 foreach(var c in customers) { Method(); } async void Method() { await SendEmailAsync(c); }
- using语句块内中使用异步方法却未使用await关键字等待完成,导致异步方法所需资源被释放
static void foo() { var newStream = new FileStream("file.txt", FileMode.Create); using (var stream = new FileStream("newfile.txt", FileMode.Open)) { // 因为未使用await等待完成,所以线程会继续执行后面的代码,导致离开using语句块 // 进而导致stream对象被释放,从而引发异常 newStream.CopyToAsync(stream); } } //改为 static async Task foo() { var newStream = new FileStream("file.txt", FileMode.Create); using (var stream = new FileStream("newfile.txt", FileMode.Open)) { await newStream.CopyToAsync(stream); } }
- await关键字禁止在catch块和finally中使用,因为可能在catch和finally中再次引发异常
// 禁止 try { page = await webClient.DownloadStringTaskAsync("http://google.com"); } catch (WebException) { page = await webClient.DownloadStringTaskAsync("http://googlemirror.com"); } // 应该用下面写法替换 bool failed = false; try { page = await webClient.DownloadStringTaskAsync("http://google.com"); } catch (WebException) { failed = true; } if (failed) { page = await webClient.DownloadStringTaskAsync("http://googlemirror.com"); }
- await关键字不能修饰 lock语句块,可以考虑使用actor模式的框架例如orleans或者dapr只让一个线程管理一批数据或者使用dataflow并行流水线处理,或者使用https://github.com/StephenCleary/AsyncEx
// 禁止,也无法通过编译 lock(obj) { await MethodAsync(); } // 可以用以下代码替代或者考虑重新设计程序 lock (obj) { // Prepare for async operation } int someValue = await MethodAsync(); lock (obj) { // Use result of async operation }
- LINQ Query SyntaxLINQ查询语法无法使用await关键字,需要转换为LINQ Method SyntaxLINQ方法语法。但是不推荐在LINQ中使用await,这样写出来的代码实际执行效果可能和预期有出入
// 这种类似sql的语法无法使用await关键字 IEnumerable<int> transformed = from x in alexsInts where x != 9 select x + 2; // 可以转换为方法语法在lambda中使用await关键字 IEnumerable<Task<int>> tasks = alexsInts .Where(x => x != 9) .Select(async x => await DoSomthingAsync(x) + await DoSomthingElseAsync(x)); IEnumerable<int> transformed = await Task.WhenAll(tasks);
-
async XXXAsync异步方法禁止使用ref out参数。其实不止异步方法不要用ref out,其它普通方法也强烈建议不要用ref out
-
创建TaskCompletionSource时建议参数使用TaskCreationOptions.RunContinuationsAsynchronously属性,将后续任务交给线程池进行处理防止死锁
-
建议使用CancellationTokenSource进行超时管理时总是释放(dispose),用于进行超时的CancellationTokenSource如果不释放,则会增加timer queue(计时器队列)的压力
总结
高级进阶(选修)
自定义可以使用await关键字的类型
只要使用类中包含XXXAwaiter GetAwaiter()方法,而XXXAwaiter又实现了INotifyCompletion接口那么就可以使用await关键字
class MyAwaitableClass { public MyAwaiter GetAwaiter() { } } class MyAwaiter : INotifyCompletion { public bool IsCompleted { get { ... } } public void OnCompleted(Action continuation) { ... } public void GetResult() { ... } }
自定义TAP方法
创建一个TaskCompletionSource
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state) { var tcs = new TaskCompletionSource<int>(); stream.BeginRead(buffer, offset, count, ar => { try { tcs.SetResult(stream.EndRead(ar)); } catch (Exception exc) { tcs.SetException(exc); } }, state); return tcs.Task; }
参考资料
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
https://docs.microsoft.com/en-us/dotnet/standard/async
https://devblogs.microsoft.com/dotnet/configureawait-faq
https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md
原文链接:https://www.cnblogs.com/KSPT/p/16712547.html