隨着智能手機的普及,現在的互聯網用戶基數動輒數以千萬、億計,這對軟件系統的執行效率和穩定性提出了更高的要求,
代碼的執行效率除了在硬件層面解決之外,在軟件層面也有很多技術出現,異步編程就是其中之一,
C# 5.0 引入一個新特性來構建異步方法------async/await,接下來我們就來學習如何靈活的使用它。
一、預備知識
1. 什么是進程(Process)?
進程(Process)是操作系統進行資源分配和調度的基本單位,這些資源包括虛地址空間、文件句柄、寄存器等。
2. 什么是線程(Thread)?
線程(Thread)是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位,
它代表了真正執行的程序,一旦進程(Process)建立,系統會在Main方法的第一行語句處開始線程(Thread)的執行。
3. 進程(Process)和線程(Thread)的關系?
- 默認情況下,一個進程(Process)只包含一個線程(Thread),從程序的開始一直執行到結束。
- 一個進程(Process)可能包含不同狀態的多個線程(Thread),它們並行執行程序的不同部分。
- 如果一個進程(Process)擁有多個線程(Thread),它們將共享進程(Process)的資源。
4. 同步(Synchronous)和異步(Asynchronous)
- 程序從第一條語句開始按順序執行到最后一條叫同步(Synchronous)。
- 程序代碼不需要按照編寫順序嚴格執行叫異步(Asynchronous)。
或者換個說法:
- 同步(Synchronous)就是調用某個方法時得等待這個調用返回結果才能繼續往后執行。
- 異步(Asynchronous)就是調用某個方法時調用方不必立刻得到結果就可繼續執行后續操作,
被調用者通過狀態、通知或回調來通知調用方。
到目前為止,本博客中 .net5 core webapi 項目實戰系列文章都是同步編程。
二、如何用 async/await 進行異步編程?
異步編程從創建一個異步方法開始,規則如下:
- 方法頭中包含 async 修飾符。
- 方法體中包含至少一個 await 表達式,表示可以異步完成的任務。
- 方法的返回類型必須是一下4種之一,其中泛型T表示實際的返回類型。
■ void ■ Task ■ Task<T> ■ ValueTask<T> - 方法形參不能有 out 或 ref 修飾符。
- 方法名應該以 Async 結尾(這是約定不是必須)。
- 如果方法體中沒有 await 表達式而標記為 async 那么該方法會以同步方式執行。
三、編碼演示。
1. 在項目中新建一個 API 控制器 AsyncDemoController ,在里面寫一個簡單的異步方法 Demo0(),代碼如下 :
private async void Demo0() { WebClient web = new WebClient(); await web.DownloadFileTaskAsync("http://www.baidu.com","abc.png"); }
功能:用.NET的 WebClient 實例下載百度網上的一張圖片(這張圖片abc.png可能並不存在,只是為了演示用)。
可以看到此方法的方法頭有 async 修飾,方法體有 await 表達式,無返回值。
2. 再來看一個帶返回值的異步方法 Demo1( ) ,代碼如下:
private async Task<string> Demo1() { WebClient web = new WebClient(); string str = await web.DownloadStringTaskAsync(new Uri("http://www.baidu.com")); return str; }
此方法頭有 async 修飾,方法體有 await 表達式,返回值是字符串,所以方法頭中的返回值類型是 Task<string>,
用泛型的 Task 來包裝實際的返回值類型(調用方獲取返回值用 Task實例的 Result 屬性就可以了)。
在控制器 AsyncDemoController 中增加一個終結點來測試這個異步方法:
[HttpGet] [Route("async1")] public string Async1() { Task<string> task = Demo1(); return task.Result; }
打開 POSTMAN,用 GET 方式訪問網址:http://localhost:61946/api/asyncdemo/async1,得到如下結果:
我們得到了百度首頁的HTML代碼(爬蟲程序就是這樣抓取網頁內容后提取所需信息的)。
3. 在 Demo1( ) 方法中,await 表達式修飾了方法 "web.DownloadStringTaskAsync(new Uri("http://www.baidu.com"));",如下:
private async Task<string> Demo1() { WebClient web = new WebClient(); string str = await web.DownloadStringTaskAsync(new Uri("http://www.baidu.com")); return str; }
如果隨便寫一個返回字符串的方法是否也可以用 await 來修飾呢?,試着寫如下代碼:
系統會提示錯誤,"string" 未包含 "GetAwaiter" 的定義的錯誤提示,可見不是每個方法都可以用 await 來修飾的。
4. 如果我們有一個方法需要寫成異步的該如何做呢,有兩種方式可以實現。
第一種,使用 Task.Run( ) 方法,代碼如下:
private async Task<string> Demo21() { // 使用 Task.Run() 來執行方法 Demo22()即可使用 await 關鍵字 string str = await Task.Run<string>(Demo22);// Task.Run<>()的泛型類型與 Demo22() 的返回值類型與一致。 return str; } private string Demo22() { ·· return "Demo22"; }
增加終結點 Async2( ) 來測試此異步方法,代碼如下:
[HttpGet] [Route("async2")] public string Async2() { Task<string> task = Demo21(); return task.Result; }
打開 POSTMAN,用 GET 方法訪問網址:http://localhost:61946/api/asyncdemo/async2,得到預期的結果如下:
0
第二種,使用 GetAwaiter( ) 的方式,這種方式比較復雜,需要按約定實現一系列的屬性和方法,其代碼如下:
namespace webapidemo2 { public class MyAsyncFunction { // 方法 CallFuncAsync()可被 await 修飾,因其返回值的類型 MyTask 中包含 GetAwaiter() 方法。 public MyTask CallFuncAsync() { return new MyTask(); } } public static class MyAsyncVariable { // 給字符串增加擴展方法 GetAwaiter() 后單個字符串也可以被 await 修飾,字符串也可以換成其他類型 public static MyAwaiter GetAwaiter(this string str) { return new MyAwaiter(); } } // 自定義的類 MyTask 中要有 GetAwaiter() 方法 public class MyTask { public MyAwaiter GetAwaiter() { return new MyAwaiter(); } } // 自定義的類 MyAwaiter 中要實現 INotifyCompletion 接口並包含如下三個成員 。 public class MyAwaiter : INotifyCompletion { public void GetResult() { return; } public bool IsCompleted { get { return true; } } public void OnCompleted(Action continuation) { } } }
在 AsyncDemoController 中加入異步方法 Demo3( ) 和終結點 Async3( ) 如下:
private async Task<string> Demo3() { MyAsyncFunction func = new MyAsyncFunction(); await func.CallFuncAsync(); // 方法 CallFuncAsync() 可以被 await 修飾。 await "abc"; // 擴展方法 GetAwaiter(this string str) 讓字符串也可以被 await 修飾。 return "Demo3"; } [HttpGet] [Route("async3")] public string Async3() { Task<string> task = Demo3(); return task.Result; }
打開 POSTMAN,用 GET 方法訪問網址:http://localhost:61946/api/asyncdemo/async3 得到期望的結果:
從 MyTask 和 MyNotify 的定義可以看到,如果想要某個變量/方法可以被 await 修飾,需要滿足如下幾個條件:
1. 如果修飾變量則該變量必須擁有 GetAwaiter() 方法(此方法是變量的擴展方法),即可執行 task.GetAwaiter(); 的操作。
2. 如果修飾方法則該方法的返回值類型必須擁有 GetAwaiter() 方法。
3. GetAwaiter() 方法的返回類型必須有 " void GetResult(); " 方法 、"bool IsCompleted" 屬性、且實現了 INotifyCompletion 接口,三者缺一不可。
( 待續... )