shanzm-2020年3月7日 23:12:53
## 0.背景引入 现在的.net异步编程中,一般都是使用 **基于任务异步模式**(Task-based Asynchronous Pattern,TAP)实现异步编程 可参考[微软文档:使用基于任务的异步模式](https:/https://img.qb5200.com/download-x/docs.microsoft.com/zh-cnhttps://img.qb5200.com/download-x/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern) 基于任务的异步模式,是使用` System.Threading.Tasks.Task` 命名空间中的` System.Threading.Tasks.Task
## 1.async和await基本语法 #### 1.1 简介 在C#5.0(.net 4.5)中添加了两个关于异步编程的关键字`async`和 `await` 两个关键字可以快速的创建和使用异步方法。`async`和`await`关键字只是编译器功能,编译后会用Task类创建代码,实现异步编程。其实只是C#对异步编程的语法糖,本质上是对Task对象的包装操作。 所以,如果不使用这两个关键字,也可以用C#4.0中Task类的方法来实现同样的功能,只是没有那么方便。(见:[.NET异步程序设计之任务并行库](https://www.cnblogs.com/shanzhiming/p/12315548.html)) #### 1.2 具体使用方法 1. `async`:用于异步方法的函数名前做修饰符,处于返回类型左侧。async只是一个标识符,只是说明该方法中有一个或多个await表达式,async本身并不创建异步操作。 2. `await`:用于异步方法内部,用于指明需要异步执行的操作(称之为**await表达式**),注意一个异步方法中可以有多个await表达式,而且应该至少有一个(若是没有的话编译的时候会警告,但还是可以构建和执行,只不过就相当于同步方法而已)。 其实这里你有没有想起Task类中为了实现延续任务而使用的等待者,比如:使用`task.GetAwaiter()`方法为task类创建一个等待者(可以参考;[3.3.2使用Awaiter](https://www.cnblogs.com/shanzhiming/p/12315548.html#%E4%B8%BAtask%E6%B7%BB%E5%8A%A0%E5%BB%B6%E7%BB%AD%E4%BB%BB%E5%8A%A1))。`await`关键字就是基于 .net 4.5中的awaiter对象实现的。 3. 总而言之,若是有`await`表达式则函数一定要用`async`修饰,若是使用了`async`修饰的方法中没有`await`表达式,编译的时候会警告! #### 1.3 返回值类型 0. 使用`async`和`await`定义的异步方法的返回值只有三种:`Task
## 2.异步方法的执行顺序 依旧是上面的示例,我们在每个操作中、操作前、操作后都打印其当前所处的线程,仔细的观察,异步方法的执行顺序。 再次强调,这里用async修饰的方法,称之为**异步方法**,这里调用该异步方法的方法,称之为**调用方法** 代码: ```cs //调用方法 static void Main(string[] args) { Console.WriteLine($"-1-.正在执行的线程,线程ID:{Thread.CurrentThread.ManagedThreadId,2}------------------调用方法中调用异步方法之前的代码"); Task
## 3.取消一个异步操作 具体可参考:[.net异步编程值任务并行库-3.6取消异步操作](https://www.cnblogs.com/shanzhiming/p/12315548.html#%E5%8F%96%E6%B6%88%E5%BC%82%E6%AD%A5%E6%93%8D%E4%BD%9C) 原理是一样的,都是使用`CancellationToken`和`CancellationTokenSource`两个类实现取消异步操作 看一个简单的示例: ```cs static void Main(string[] args) { CancellationTokenSource cts = new CancellationTokenSource();//生成一个CancellationTokenSource对象, CancellationToken ct = cts.Token;//该对象可以创建CancellationToken对象,即一个令牌(token) Task result = DoAsync(ct, 50); for (int i = 0; i <= 5; i++)//主线程中的循环(模拟在异步方法声明之后的工作) { Thread.Sleep(1000); Console.WriteLine("主线程中的循环次数:" + i); } cts.Cancel();//注意在主线程中的循环结束后(5s左右),运行到此处, //则此时CancellationTokenSource对象中的token.IsCancellationRequested==true //则在异步操作DoAsync()中根据此判断,则取消异步操作 Console.ReadKey(); CancellTask(); CancellTask2(); } //异步方法:取消异步操作的令牌通过参数传入 static async Task DoAsync(CancellationToken ct, int Max) { await Task.Run(() => { for (int i = 0; i <= Max; i++) { if (ct.IsCancellationRequested)//一旦CancellationToken对象的源CancellationTokenSource运行了Cancel();此时CancellationToken.IsCancellationRequested==ture { return; } Thread.Sleep(1000); Console.WriteLine("次线程中的循环次数:" + i); } }/*,ct*/);//这里取消令牌可以作为Task.Run()的第二个参数,也可以不写! } ```
## 4.同步和异步等待任务 #### 4.1 在调用方法中同步等待任务 “调用方法可以调用任意多个异步方法并接收它们返回的Task对象。然后你的代码会继续执行其他任务,但在某个点上可能会需要等待某个特殊Task对象完成,然后再继续。为此,Task类提供了一个实例方法wait,可以在Task对象上调用该方法。”--《C#图解教程》 [使用`task.Wait();`等待异步任务task完成](https://www.cnblogs.com/shanzhiming/p/12315548.html#%E5%88%9B%E5%BB%BA%E6%97%A0%E8%BF%94%E5%9B%9E%E5%80%BC%E7%9A%84task%E4%BB%BB%E5%8A%A1): `Wait`方法用于单一Task的对象。若是想要等待多个Task,可以使用Task类中的两个静态方法,其中`WaitAll`等待所有任务都结束,`WaitAny`等待任一任务结束。 示例:使用Task.WaitAll()和Task.WaitAny() ```cs static void Main(string[] args) { Console.WriteLine($"当前线程ID:{Thread.CurrentThread.ManagedThreadId,2 }:Task之前..."); Task
#### 4.2 在调用方法中异步等待任务 #### 4.2.1使用await等待异步任务 其实在一个方法中调用多个异步方法时候,当某个异步方法依赖于另外一个异步方法的结果的时候,我们一般是在每一个调用的异步方法处使用`await`关键字等待该异步操作的结果,但是这样就会出现`async`传染。 `await`不同于`Wait()`,`await`等待是异步的,不会阻塞线程,而`Wait()`会阻塞线程 注意如无必用,或是不存在对某个异步操作的等待,尽量不要使用`await`,直接把异步操作的返回值给`Task
## 5.异步操作中的异常处理 #### 5.1 异常处理 一般程序中对异常的处理使用`try{……} catch{……}` 首先看一个捕获异常失败的示例: 在Main()中调用`ThrowEx(2000,"这是异常信息")`,第一个参数是ThrowEx中的Tast延迟的时间,第二个参数是ThrowEx中的抛出异常的信息。 ```cs static void Main(string[] args) { try { ThrowEx(2000, "这是异常信息"); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadKey(); } private async static Task ThrowEx(int ms, string message)//注意这里的返回值类型为Task,若是写成void也是无法在catch语句中捕获异常,但是运行vs会报错(见:说明2) { await Task.Delay(ms).ContinueWith(t => Console.WriteLine("hello word")); throw new Exception(message); } ``` **说明1:** 多打断点,就可以发现为何捕捉不到异常了。 因为当调用`ThrowEx(2000, "异常信息")`,开始异步方法中的await表达式, 即创建一个新的线程,在后台执行await表达式,而主线程中此时会继续执行`ThrowEx(2000, "异常信息"); `后的代码:`catch (Exception ex)`, 此时,异步方法中还在等待await表达式的执行,还没有抛出我们自己定义的异常,所以此时压根就没有异常抛出,所以catch语句也就捕获不到异常, 而当异步方法抛出异常,此时主线程中catch语句已经执行完毕了! **说明2:** 在**1.基本语法-返回值类型**中我们说道:在编写异步方法的时候,有时后没有返回值,也不需要查看异步操作的状态,我们设置返回值类型为`void`,而且称之为“调用并忘记”。然而这种异步代码编写方式,并不值得提倡。 为什么呢?若是没有返回值,异步方法中抛出的异常就无法传递到主线程,在主线程中的`catch`语句就无法捕获拍异常!所以**异步方法最好返回一个Task类型**。 异步方法有返回值的时候,抛出的在异常会置于Task对象中,可以通过task.IsFlauted属性查看是否有异常,在主线程的调用方法中,使用`catch`语句可以捕获异常! 正确示例:只需要给调用的异步方法,添加一个`await`。 ```cs static void Main(string[] args) { try { await ThrowEx(2000, "这是异常信息"); } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.ReadKey(); } private async static Task ThrowEx(int ms, string message) { await Task.Delay(ms).ContinueWith(t => Console.WriteLine("hello word")); throw new Exception(message); } ``` #### 5.2 多个异步方法的异常处理 使用`Task.WhenAll()`处理多个异步方法中抛出异常 当有多个异步操作,使用WhenAll异步等待,其返回值是一个Task类型对象,该对象的异常为`AggregateException`类型的异常,每一个的子任务(即WhenAll所等待的所有任务)抛出的异常都是包装在这一个AggregateException中,若是需要打印其中的异常,则需要遍历`AggregateException.InnerExceptions` ```cs static void Main(string[] args) { Task taskResult = null;//注意因为在catch语句中需要使用这个WhenAll的返回值,所以定义在try语句之外。 try { Console.WriteLine($"当前的线程Id:{Thread.CurrentThread.ManagedThreadId,2}:do something before task"); Task t1 = ThrowEx($"这是第一个抛出的异常信息:异常所在线程ID:{Thread.CurrentThread.ManagedThreadId,2}", 3000); Task t2 = ThrowEx($"这是第二个抛出的异常信息:异常所在线程ID:{Thread.CurrentThread.ManagedThreadId,2}", 5000); await (taskResult = Task.WhenAll(t1, t2)); } catch (Exception)//注意这里捕获的异常只是WhenAll()等待的异步任务中第一抛出的异常 { foreach (var item in taskResult.Exception.InnerExceptions)//通过WhenAll()的返回对象的Exception属性来查阅所有的异常信息 { Console.WriteLine($"当前的线程Id:{Thread.CurrentThread.ManagedThreadId,2}:{item.Message}"); } } } private async static Task ThrowEx(int ms, string message) { await Task.Delay(ms).ContinueWith(t => Console.WriteLine("hello word")); throw new Exception(message); } ``` 运行结果: ![](https://img2020.cnblogs.com/blog/1576687/202003/1576687-20200307231800069-161393712.png) **说明** : `Task.WhenAll()`返回的Task对象中的Exception属性是`AggregateException`类型的异常. 注意,该访问该异常`InnerExcption`属性则只包含第一个异常,该异常的`InnerExcptions`属性,则包含所有子任务异常. #### 5.3 AggregateException中的方法 首先多个异步操作的异常会包装在一个AggregateException异常中,被包装的异常可以也是AggregateException类型的异常,所以若是需要打印异常信息可能需要循环嵌套,比较麻烦。 故可以使用 `AggregateException.Flatten()`打破异常的嵌套。 **注意,凡是使用`await`等待的异步操作,它抛出的异常无法使用`catch(AggregateException)`捕获!** 只能使用`catch (Exception)`对异常捕获,在通过使用Task的返回值的Exception属性对异性进行操作。 当然你要是想使用`catch(AggregateException)`捕获到异常,则可以使用task.Wait()方法等待异步任务,则抛出的异常为AggregateException类型的异常 示例:([完整Demo](https://github.com/shanzm/AsynchronousProgramming/blob/master/009AggregateException%E4%B8%AD%E6%96%B9%E6%B3%95/Program.cs)) ```cs catch (AggregateException ae)//AggregateException类型异常的错误信息是“发生一个或多个异常” { foreach (var exception in ae.Flatten().InnerExceptions) //使用AggregateException的Flatten()方法,除去异常的嵌套,这里你也可以测试不使用Flatten(),抛出的信息为“有一个或多个异常” { if (exception is TestException) { Console.WriteLine(exception.Message); } else { throw; } } } ``` 若是需要针对`AggregateException`中某个或是某种异常进行处理,可以使用`Handle()`方法 `Handel()`的参数是一个有返回值的委托:`Func
## 6.多线程和异步的区分 不要把**多线程**和**异步**两个概念混为一谈!**异步是最终目的,多线程只是我们实现异步的一种手段!** 首先,使用异步和多线程都可以避免线程的阻塞,但是原理是不一样的。 多线程:当前线程中创建一个新的线程,当前线程线程则不会被锁定了,但是锁定新的线程执行某个操作。换句话说就是换一条线程用来代替原本会被锁定的主线程!优点就是,线程中的处理程序的运行顺序还是从上往下的,方便理解,但是线程间的共享变量可能造成死锁的出现。 异步:异步概念是与同步的概念相对的,**简单的说就是:调用者发送一个调用请求后,调用者不需要等待被被调用者返回结果而可以继续做其他的事情**。实现异步一般是通过多线程,但是还可以通过多进程实现异步! 多线程和异步可以解决不同的问题 但是首先我们要区分当前需要长时间操作的任务是:CPU密集型还是IO密集型,具体可参考[长时间的复杂任务又分为两种](https://www.cnblogs.com/shanzhiming/p/12292710.html#%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B) CPU Bound:使用多线程 IO Bound:使用异步
## 7. 在 .NET MVC中异步编程 现在的 ASP .NET MVC项目中,若使用的.net中的方法有异步版本的就尽量使用异步的方法。 在MVC项目中异步编程可以大大的提高网站服务器的吞吐量,即可以可以大大的提高网站的同时受理的请求数量 据传,MVC网站若是异步编程则可以提高网站的同时访问量的2.6倍。 注意是提高网站的同时访问量,而不是提高网站的访问速度! 在MVC项目异步编程的的方式和在控制台中一样,使用async和await,基于任务的异步编程模式 简单的示例:同步和异步两种方式分别读取桌面1.txt文件 ```cs //同步操作 public ActionResult Index() { string msg = ""; using (StreamReader sr = new StreamReader(@"C:\Users\shanzm\Desktop\1.txt", Encoding.Default)) { while (!sr.EndOfStream) { msg = sr.ReadToEnd(); } } return Content(msg); } //异步操作 public async Task Index2() { string msg = ""; using (StreamReader sr = new StreamReader(@"C:\Users\shanzm\Desktop\1.txt", Encoding.Default)) { while (!sr.EndOfStream) { msg = await sr.ReadToEndAsync();//使用异步版本的方法 } } return Content(msg); } ```
## 8. 参考及示例源码 [源码:点击下载](https://github.com/shanzm/AsynchronousProgramming) [书籍:C#高级编程]() [书籍:精通C#]() [微软:基于任务的异步编程](https:/https://img.qb5200.com/download-x/docs.microsoft.com/zh-cnhttps://img.qb5200.com/download-x/dotnet/standard/parallel-programming/task-based-asynchronous-programming) [微软:异常处理(任务并行库)](https:/https://img.qb5200.com/download-x/docs.microsoft.com/zh-cnhttps://img.qb5200.com/download-x/dotnet/standard/parallel-programming/exception-handling-task-parallel-library#using-the-handle-method-to-filter-inner-exceptions)