.NET非同步程式設計之async&await
阿新 • • 發佈:2020-03-08
[TOC]
shanzm-2020年3月7日 23:12:53
## 0.背景引入 現在的.net非同步程式設計中,一般都是使用 **基於任務非同步模式**(Task-based Asynchronous Pattern,TAP)實現非同步程式設計 可參考[微軟文件:使用基於任務的非同步模式](https://docs.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern) 基於任務的非同步模式,是使用` System.Threading.Tasks` 名稱空間中的` System.Threading.Tasks.Task` 和 `System.Threading.Tasks .Task`型別實現非同步程式設計,
配合著C#5.0(.net 4.5)中新增的兩個關於非同步程式設計的關鍵字`async`和 `await`,可以快速的實現非同步操作。
## 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`、`Task`、`void`
1. 非同步方法中有return語句,則返回值型別為`Task`,其中`T`是return語句返回物件的型別。(編譯器幫我們把`T`型別資料轉換為`Task`型別)。所以不要使用return返回一個`Task`型別的物件,而是隻需要返回一個`T`型別資料即可
2. 非同步方法中沒有return語句,但是你需要檢視非同步方法的狀態或是要對Task物件操作(比如task.Wait()),則可定義返回值型別為`Task`。
3. 非同步方法中沒有return語句且不需要檢視非同步方法的狀態,則可認為是返回值是`void`型別。此時稱之為“**呼叫並忘記**”(fire and forget),其實這並不是一個很好的用法(具體的分析可以看:非同步方法的異常處理)!
4. 若是真的需要返回一個`Task`型別的資料 (比如在非async修飾的方法中,定義返回值型別就是一個`Task`型別),則:
* `return Task.Run(()=>{……})`
* 將`T`型別轉換為`Task`型別:`return Task.FromResult(T data)`
* 將`IAsyncResult`型別物件轉換為Task型別:`return Task.Factory.FromTask(IAsyncResult data)`
#### 1.4 其他細節
1. `async`修飾符**只能**用於返回`Task`、`Task`和`viod`的方法,或者Lambda表示式中。
2. ~~`async`不能用於程式的入口點,即Main()不能使用`async`修飾符。~~ 謝謝@coredx提醒:C#7.1中應用程式的入口點可以具有async修飾符,[參考:What's new in C# 7.1](https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7-1)
#### 1.5 async傳染性
1. 簡單的說:函式體中含有`await`表示式的函式,必須使用`async `修飾!
而一個使用了`async`修飾的方法,在呼叫它的時候如有必要則必須使用`await`等待!
使用了`await`等待的方法,則其呼叫方法又必須使用`async`修飾,這從而形成了一個迴圈,這就是**async傳染**
換句話說就是哪裡呼叫了`async`修飾的方法則`async`就會傳染到哪!
可以有的時候我們並不想要我們的方法變為`async`修飾的方法,所以需要避免`async`傳染
避免的主要方法就是使用**延續任務**來避免,你想一想之前在Task類中,使用延續任務時,主要就是避免使用`await`等待!
[參考:C# 5.0中同步執行非同步方法](https://www.kechuang.org/t/79529)
2. 之前我們說了:~~主函式Main()不能使用`async`修飾符,因為Main函式不能夠是非同步方法,這也就意味著一切的非同步方法最終的呼叫方法一定是同步方法~~(C#7.1Main可以是非同步的),而呼叫非同步方法的那個同步方法,稱之為**病源隔斷方法**,因為在這裡開始,不再會發生async傳染。
#### 1.6 簡單示例
示例1:定義一個非同步方法,並呼叫它
```cs
static void Main(string[] args)
{
Task t = SumAsync(1, 2);
t.ContinueWith(t => Console.WriteLine( "非同步操作結果為:" + t.Result));
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"迴圈次數{i}");
}
Console.ReadKey();
}
private static async Task SumAsync(int num1, int num2)
{
int sum = await Task.Run(() => { Thread.Sleep(3000); return num1 + num2; });
return sum;
}
```
**示例說明** :
1. 非同步方法SumAsync()函式體中返回的是整型sum,即返回的是一個整型,但是在宣告函式時,返回值需寫為:`Task`
反過來說:**若是非同步方法的返回值型別為`Task`,則在方法中只需要返回`T`型別的資料**。
這一點就是和Task.Run()的返回值的書寫方式一樣,即若`Task.Run()`引數是有返回值的委託`Func`,則`Task.Run()`返回值是`Task`泛型
2. 非同步方法的命名預設是在最後加"Async",即"XXXAsync"。
3. 呼叫非同步方法的方法稱之為**呼叫方法**(在這裡就是主函式Main()),**呼叫方法和被呼叫的非同步方法,不一定在不同的執行緒中**。
4. 其實你看上面示例的程式碼也會發現,單獨把非同步操作封裝為一個非同步方法,這樣可以為非同步操作傳參!
你可以還記得的在[.net非同步程式設計之任務並行庫](https://www.cnblogs.com/shanzhiming/p/12315548.html#task%E7%B1%BB%E7%AE%80%E4%BB%8B)中,多次說明Task.Run()的引數只能是無參委託。
5. 有一點在這裡說明一下:關於非同步匿名方法,或是非同步Lambda表示式。
在為一個事件繫結事件處理程式的時候,對於一些簡單的事件處理程式,我們可以使用Lambda表示式
但是我們想要非同步操作Lambda表示式,則可以直接寫為:
```cs
Butten.Click+=async(sender,e)=>{...await...}
```
詳細可以參照《C#圖解教程:20.5使用非同步Lambda表示式》
## 2.非同步方法的執行順序 依舊是上面的示例,我們在每個操作中、操作前、操作後都列印其當前所處的執行緒,仔細的觀察,非同步方法的執行順序。 再次強調,這裡用async修飾的方法,稱之為**非同步方法**,這裡呼叫該非同步方法的方法,稱之為**呼叫方法** 程式碼: ```cs //呼叫方法 static void Main(string[] args) { Console.WriteLine($"-1-.正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}------------------呼叫方法中呼叫非同步方法之前的程式碼"); Task result = SumAsync(1, 2);
Console.WriteLine($"-3-.正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}------------------呼叫方法中呼叫非同步方法之後的程式碼");
result.ContinueWith(t => Console.WriteLine($"-8-.正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}------------------這是延續任務的執行緒" + "-非同步操作結果為:" + result.Result));
Console.WriteLine($"-4-.正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}------------------呼叫方法中延續任務之後的程式碼");
for (int i = 0; i < 5; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}"+$"迴圈次數{i}--------------------------------------呼叫方法中延續任務之後的程式碼");
}
Console.ReadKey();
}
//非同步方法
private static async Task SumAsync(int num1, int num2)
{
Console.WriteLine($"-2-.正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}------------------非同步方法中await表示式之前的程式碼");
int sum1 = await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine($"-5-.正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}------------------這是第一個await表示式的執行緒"); return num1 + num2; });
Console.WriteLine($"-6-.正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}------------------非同步方法中await表示式之後的程式碼");
int sum2=await Task.Run(() => { Thread.Sleep(3000); Console.WriteLine($"-7-.正在執行的執行緒,執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}------------------這是第二個await表示式的執行緒"); return num1 + num2; });
return sum1+sum2;
}
```
執行結果:
![](https://img2020.cnblogs.com/blog/1576687/202003/1576687-20200307231445752-1740785731.png)
**說明**:
注意執行順序:
呼叫非同步方法方法後,按照同步順序執行非同步方法中await表示式之前的程式碼,
當執行到第1個await表示式後,建立一個新的執行緒,後臺執行該await表示式,實現非同步。
第1個await表示式,未完成之前,繼續執行**呼叫函式中**的非同步方法之後的程式碼(**注意await表示式後臺未完成之前,不是繼續執行await表示式之後的程式碼,而是繼續執行呼叫函式中的非同步方法之後的程式碼**),
當第1個await表示式在後臺完成後,繼續執行**非同步方法中**第1個await表示式之後的程式碼,
當執行到第2個await表示式後,建立一個新的執行緒,後臺執行該await表示式,
第2個await表示式,未完成之前,繼續執行**呼叫函式中**被第1個await完成後打斷的的程式碼
當第2個await表示式在後臺執行完成後,繼續執行**非同步方法中**第2個await表示式之後的程式碼,
當非同步方法執行到return後,則開始**呼叫方法中**的對該非同步方法的延續任務
該延續任務和呼叫方法不在一個執行緒中,這裡有可能和第2個await表示式在同一個執行緒中,也有可能和第1個await表示式在同一個執行緒中。
## 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 t1 = DoAsync(2000);
Task t2 = DoAsync(6000);
//Task.WaitAll(t1, t2);//等待t1和t2都完畢後才進行後續的程式碼(即阻塞了主執行緒)
//Task.WaitAny(t1, t2);//等待t1和t2中有任一個完成(除錯的時候,你就會發現當t1完成後就開始執行後續的循程式碼)
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"當前執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}:迴圈中");
}
Console.ReadKey();
}
private static async Task DoAsync(int num)
{
int result = await Task.Run(() => { Thread.Sleep(num); Console.WriteLine($"當前執行緒ID{Thread.CurrentThread.ManagedThreadId,2}:非同步操作之等待:{num}s"); return num; });
return result;
}
```
**說明1** :
對程式碼中註釋掉的程式碼分別除錯,則可以發現其中的不同。
`Task.WaitAll(params Task[] tasks)`:表示停止當前主執行緒,等待Task型別陣列tasks中的所有Task操作結束,即會發生阻塞
Task.WaitAll()除錯結果:
![](https://img2020.cnblogs.com/blog/1576687/202003/1576687-20200307232000440-1447660197.gif)
**說明2**:
`Task.WaitAny(params Task[] tasks)`:表示停止當前主執行緒,等待Task型別陣列tasks中的任一個Task操作結束,也是會發生阻塞,單是阻塞主執行緒在任一個Task操作結束,之後主執行緒會繼續,其他的Task在後臺繼續
Task.WaitAny()除錯結果:
![](https://img2020.cnblogs.com/blog/1576687/202003/1576687-20200307232059311-1926134820.gif)
#### 4.2 在呼叫方法中非同步等待任務 #### 4.2.1使用await等待非同步任務 其實在一個方法中呼叫多個非同步方法時候,當某個非同步方法依賴於另外一個非同步方法的結果的時候,我們一般是在每一個呼叫的非同步方法處使用`await`關鍵字等待該非同步操作的結果,但是這樣就會出現`async`傳染。 `await`不同於`Wait()`,`await`等待是非同步的,不會阻塞執行緒,而`Wait()`會阻塞執行緒 注意如無必用,或是不存在對某個非同步操作的等待,儘量不要使用`await`,直接把非同步操作的返回值給`Task`型別的變數,可以使程式執行的更快!
其實你也注意到了:不使用`await`等待非同步操作,則非同步操作的返回值就是定義的返回值`Task`,但是使用`await`等待則非同步操作的返回值就是具體的簡單型別,比如int型別等。
換言之:**非同步方法的返回值是`Task`,則使用`await`等待可以直接獲取非同步方法的`T`型別的返回值**
示例:
```cs
static void Main(string[] args)
{
ReferencingMethodAsync();
}
//該呼叫函式也要使用async關鍵字修飾(即async傳染),因為使用了await等待,
private static async void ReferencingMethodAsync()
{
int result1 = await SumAsync(1, 2);//這裡使用了await 關鍵字則,呼叫方法MultipleMethod2()必須使用async修飾(即async傳染性)
int result2 = await SumAsync(1, result1);
Console.WriteLine(result2);
}
private static async Task SumAsync(int num1, int num2)
{
int sum = await Task.Run(() => { Thread.Sleep(3000); return num1 + num2; });
return sum;
}
```
#### 4.2.2使用WhenAll()和WhenAny()
**`Task.WhenAll()`和`Task.WhenAny()`是`Task.WaitAll()`和`Task.WaitAny()`的非同步版本,即非同步等待Task完成**
示例:使用Task.WhenAll()和Task.WhenAny()
```cs
static void Main(string[] args)
{
Console.WriteLine($"當前執行緒ID:{Thread.CurrentThread.ManagedThreadId,2 }:Task之前...");
Task t1 = DoAsync(2000);
Task t2 = DoAsync(6000);
//Task.WhenAll(t1, t2);//非同步等待t1和t2兩個完成(除錯的時候你會發現任務t1和t2都在新的執行緒中執行,主線繼續執行後續的迴圈程式碼)
//Task.WhenAny(t1, t2);//非同步等待t1和t2中任一個完成(除錯的時候你就會發現兩個任務分別在新執行緒中執行,執行緒繼續執行後續的迴圈程式碼,當t1完成後,繼續後續的迴圈程式碼)
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"當前執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}:迴圈中");
}
Console.ReadKey();
}
private static async Task DoAsync(int num)
{
int result = await Task.Run(() => { Thread.Sleep(num); Console.WriteLine($"當前執行緒ID{Thread.CurrentThread.ManagedThreadId,2}:非同步操作之等待:{num}s"); return num; });
return result;
}
```
**說明1**:
在示例中看到Task.WhenAll和Task.WhenAny的使用,但是在實際中有什麼作用呢?
首先,如前所所述,Task.WhenAll()和Task.WhenAny()是Task.WaitAll()和Task.WaitAny()的非同步版本,但是呢,Task.WaitAll()和Task.WaitAny()是沒有返回值的,`Task.WhenAll()`和`Task.WhenAny()`是有返回值,返回值型別是一個Task物件,所以你可以給其一個延續任務,即在非同步等待的Task完成後,指定繼續執行的Task。
所以當呼叫的非同步方法沒有相互的依賴的時候,一般還是使用WhenAll(),來等待非同步方法,同時也可以給所有的非同步方法結束後新增一個延續任務!
示例:為非同步等待後新增延續工作
```cs
static void Main(string[] args)
{
Console.WriteLine($"當前執行緒ID:{Thread.CurrentThread.ManagedThreadId,2 }:Task之前...");
Task t1 = DoAsync(2000);
Task t2 = DoAsync(6000);
//Task.WhenAll(t1, t2).ContinueWith(t => Console.WriteLine($"當前執行緒ID:{Thread.CurrentThread.ManagedThreadId,2 }:延續任務,兩個非同步操作返回值是一個int[],其中元素分別是{t.Result[0]}、{t.Result[1]}"));
//Task.WhenAny(t1, t2).ContinueWith(t => Console.WriteLine($"當前執行緒ID:{Thread.CurrentThread.ManagedThreadId,2 }:延續任務,第一個完成的非同步操作返回值是{t.Result.Result}"));
for (int i = 0; i < 8; i++)
{
Thread.Sleep(1000);
Console.WriteLine($"當前執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}:迴圈中");
}
Console.ReadKey();
}
private static async Task DoAsync(int num)
{
int result = await Task.Run(() => { Thread.Sleep(num); Console.WriteLine($"當前執行緒ID:{Thread.CurrentThread.ManagedThreadId,2}:非同步操作之等待:{num}s"); return num; });
return result;
}
```
**說明1**:
若是`Task.WhenAll()`後的延續工作,則注意`Task.WhenAll()`的返回的`Task`的`Result`是`TResult[]`型別
即多個Task的返回值,存放在一個數組中
執行結果:
![](https://img2020.cnblogs.com/blog/1576687/202003/1576687-20200307231639171-80345781.gif)
**說明2**:
若是`Task.WhenAny()`後的延續工作,則注意`Task.WhenAny()`的返回的是`Task`型別,即其`Result`是`Task`型別,所以為了獲取第一結束的Task的返回值,需要:`t.Result.Result`。
執行結果:
![](https://img2020.cnblogs.com/blog/1576687/202003/1576687-20200307231711372-1347117219.gif)
**說明3**:
`Task.WhenAll(Task[] tasks).ContinueWith(Action)`
等價於`Task.Factory.ContinueWhenAll(Task[] tasks, Action) `
`Task.WhenAny(Task[] tasks).ContinueWith(Action)`
等價於`Task.Factory.ContinueWhenAny(Task[] tasks, Action)`
## 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 predicate`
示例:([完整Demo](https://github.com/shanzm/AsynchronousProgramming/blob/8c36d6a90954c5a14d93d29b7f68b4c6d47af6f5/009AggregateException%E4%B8%AD%E6%96%B9%E6%B3%95/Program.cs#L95))
```cs
catch (Exception)
{
t.Exception.Handle(e =>
{
if (e is TestException)//如果是TestException型別的異常
{
Console.WriteLine(e.Message);
}
return e is TestException;
});
}
```
## 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://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/task-based-asynchronous-programming) [微軟:異常處理(任務並行庫)](https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/exception-handling-task-parallel-library#using-the-handle-method-to-filter-inner-exceptions)
## 0.背景引入 現在的.net非同步程式設計中,一般都是使用 **基於任務非同步模式**(Task-based Asynchronous Pattern,TAP)實現非同步程式設計 可參考[微軟文件:使用基於任務的非同步模式](https://docs.microsoft.com/zh-cn/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern) 基於任務的非同步模式,是使用` System.Threading.Tasks` 名稱空間中的` 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://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/task-based-asynchronous-programming) [微軟:異常處理(任務並行庫)](https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/exception-handling-task-parallel-library#using-the-handle-method-to-filter-inner-exceptions)