怎樣使用 async & await 一步步將同步代碼轉換為非同步編程 【博主】反骨仔 【出處】http://www.cnblogs.com/liqingwen/p/6079707.html 序 上次,博主通過《利用 async & await 的非同步編程》該篇點睛之作介紹了 async & awai ...
怎樣使用 async & await 一步步將同步代碼轉換為非同步編程
【博主】反骨仔 【出處】http://www.cnblogs.com/liqingwen/p/6079707.html
序
上次,博主通過《利用 async & await 的非同步編程》該篇點睛之作介紹了 async & await 的基本用法及非同步的控制流和一些其它的東西。
今天,博主打算從創建一個普通的 WPF 應用程式開始,看看如何將它逐步轉換成一個非同步的解決方案。
目錄
介紹
這裡通過一個普通的 WPF 程式進行講解:
只是一個文本框和一個按鈕,左邊文本框的內容為點擊右鍵按鈕時所產生的結果。
添加引用
demo 可能需要用到的部分 using 指令:
using System.IO; using System.Net; using System.Net.Http; using System.Threading;
先創建一個同步的 WPF
1.這是右邊點擊按鈕的事件:
1 /// <summary> 2 /// 點擊事件 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private void btnSwitch_Click(object sender, RoutedEventArgs e) 7 { 8 //清除文本框所有內容 9 tbResult.Clear(); 10 11 //統計總數 12 SumSizes(); 13 }
2.我在 SumSizes 方法內包含幾個方法:
① InitUrlInfoes:初始化 url 信息列表;
② GetUrlContents:獲取網址內容;
③ DisplayResults:顯示結果。
(1)SumSizes 方法:統計總數。
1 /// <summary> 2 /// 統計總數 3 /// </summary> 4 private void SumSizes() 5 { 6 //載入網址 7 var urls = InitUrlInfoes(); 8 9 //位元組總數 10 var totalCount = 0; 11 foreach (var url in urls) 12 { 13 //返回一個 url 內容的位元組數組 14 var contents = GetUrlContents(url); 15 16 //顯示結果 17 DisplayResults(url, contents); 18 19 //更新總數 20 totalCount += contents.Length; 21 } 22 23 tbResult.Text += $"\r\n Total: {totalCount}, OK!"; 24 }View Code
(2)InitUrlInfoes 方法:初始化 url 信息列表。
1 /// <summary> 2 /// 初始化 url 信息列表 3 /// </summary> 4 /// <returns></returns> 5 private IList<string> InitUrlInfoes() 6 { 7 var urls = new List<string>() 8 { 9 "http://www.cnblogs.com/", 10 "http://www.cnblogs.com/liqingwen/", 11 "http://www.cnblogs.com/liqingwen/p/5902587.html", 12 "http://www.cnblogs.com/liqingwen/p/5922573.html" 13 }; 14 15 return urls; 16 }View Code
(3)GetUrlContents 方法:獲取網址內容。
1 /// <summary> 2 /// 獲取網址內容 3 /// </summary> 4 /// <param name="url"></param> 5 /// <returns></returns> 6 private byte[] GetUrlContents(string url) 7 { 8 //假設下載速度平均延遲 300 毫秒 9 Thread.Sleep(300); 10 11 using (var ms = new MemoryStream()) 12 { 13 var req = WebRequest.Create(url); 14 15 using (var response = req.GetResponse()) 16 { 17 //從指定 url 里讀取數據 18 using (var rs = response.GetResponseStream()) 19 { 20 //從當前流中讀取位元組並將其寫入到另一流中 21 rs.CopyTo(ms); 22 } 23 } 24 25 return ms.ToArray(); 26 } 27 28 }View Code
(4)DisplayResults 方法:顯示結果
1 /// <summary> 2 /// 顯示結果 3 /// </summary> 4 /// <param name="url"></param> 5 /// <param name="content"></param> 6 private void DisplayResults(string url, byte[] content) 7 { 8 //內容長度 9 var bytes = content.Length; 10 11 //移除 http:// 首碼 12 var replaceUrl = url.Replace("http://", ""); 13 14 //顯示 15 tbResult.Text += $"\r\n {replaceUrl}: {bytes}"; 16 }View Code
測試結果圖
全部顯示需要耗費數秒的時間。在點擊的同時,它在等待請求資源下載時,UI 線程阻塞。因此,在點擊“啟動”按鈕後,將無法移動,最大化,最小化,甚至關閉顯示視窗。直到結果顯示之前這些操作都會沒有效果。如果網站沒有響應時,您沒有站點失敗的表示形式。即使不想再繼續等待並關閉程式都會很困難。
將上面的 demo 逐步轉換為非同步方法
1.GetUrlContents 方法 => GetUrlContentsAsync 非同步方法
(1) 將 GetResponse 方法改成 GetResponseAsync 方法:
//var response = req.GetResponse(); var response = req.GetResponseAsync()
(2)在 GetResponseAsync 方法前加上 await:
GetResponseAsync 將返回 Task。 在這種情況下,任務返回變數 TResult,具有類型 WebResponse。
從任務若要檢索 WebResponse 值,將 await 運算符應用於調用的 GetResponseAsync 方法。
//var response = req.GetResponseAsync() var response = await req.GetResponseAsync()
await 運算符掛起當前方法,直到等待的任務完成。同時,控制權返回到當前方法的調用方。在這裡,當前方法是 GetUrlContents,因此,調用方是 SumSizes。當任務完成時,將提交的 WebResponse 對象生成,將等待的任務的值分配給 response。
上面的內容也可以拆分成下麵的內容:
//Task<WebResponse> responseTask = req.GetResponseAsync(); //var response = await responseTask;
responseTask 為 webReq.GetResponseAsync 的調用返回 Task 或 Task<WebResponse>。 然後 await 運算符應用於 task 檢索 WebResponse 值。
(3)由於在上一步中添加了 await 運算符,編譯器會報告錯誤。await 運算符在標有 async 的方法下才能使用。當您重覆轉換步驟替換 CopyTo 為 CopyToAsync 時,請先暫時忽略該錯誤。
-
更改調用 CopyToAsync方法的名稱。
-
CopyTo 或 CopyToAsync 方法複製位元組為其參數,不返回有意義的值。在同步版本中,CopyTo 的調用不返回值。在非同步版本中,即CopyToAsync,返回 Task,可應用 await 於方法 CopyToAsync。
//rs.CopyTo(ms); await rs.CopyToAsync(ms);
(4)也要修改 Tread.Sleep。Thread.Sleep 是同步延遲,Task.Delay 非同步延遲;Thread.Sleep 會阻塞線程,而Task.Delay 不會。
//Thread.Sleep(300); await Task.Delay(300);
(5)在 GetUrlContents 仍然要修改的只是調整方法簽名。在標有非同步的方法只能使用 await 運算符 async 修飾符。添加 async 修飾符標記方法作為非同步方法 。
//private async byte[] GetUrlContents(string url) //private async Task<byte[]> GetUrlContents(string url) private async Task<byte[]> GetUrlContentsAsync(string url)
非同步方法的返回類型只能 Task<T>、Task 或 void。 通常 void 的返回類型僅在非同步事件處理程式中使用。在某些情況下,您使用 Task<T>,如果返回類型 T 的值的完整方法具有 return 語句以及使用 Task,但是已完成方法不返回有意義的值。可以將 Task 返回類型理解為“任務 (失效)”。
方法 GetURLContents 具有返回語句,因此,該語句返回位元組數組。 這裡,非同步版本的返回類型為 Task<T>,T 為位元組數組。在方法簽名中進行以下更改:
-
返回類型更改 Task<byte[]>。
-
按照約定,非同步方法是以“Async”結尾的名稱,因此可對方法 GetURLContentsAsync 重命名。
(6)這是修改後的整體方法
1 /// <summary> 2 /// 獲取網址內容 3 /// </summary> 4 /// <param name="url"></param> 5 /// <returns></returns> 6 /// <remarks> 7 /// private async byte[] GetUrlContents(string url) 8 /// private async Task<byte[]> GetUrlContents(string url) 9 /// </remarks> 10 private async Task<byte[]> GetUrlContentsAsync(string url) 11 { 12 //假設下載速度平均延遲 300 毫秒 13 await Task.Delay(300); 14 15 using (var ms = new MemoryStream()) 16 { 17 var req = WebRequest.Create(url); 18 19 //var response = req.GetResponse(); 20 //Task<WebResponse> responseTask = req.GetResponseAsync(); 21 //var response = await responseTask; 22 23 using (var response = await req.GetResponseAsync()) 24 { 25 //從指定 url 里讀取數據 26 using (var rs = response.GetResponseStream()) 27 { 28 //從當前流中讀取位元組並將其寫入到另一流中 29 //rs.CopyTo(ms); 30 await rs.CopyToAsync(ms); 31 } 32 } 33 34 return ms.ToArray(); 35 } 36 }GetUrlContentsAsync 方法
2.仿造上述過程將 SumSizes 方法 => SumSizesAsync 非同步方法。
1 /// <summary> 2 /// 非同步統計總數 3 /// </summary> 4 private async Task SumSizesAsync() 5 { 6 //載入網址 7 var urls = InitUrlInfoes(); 8 9 //位元組總數 10 var totalCount = 0; 11 foreach (var url in urls) 12 { 13 //返回一個 url 內容的位元組數組 14 var contents = await GetUrlContentsAsync(url); 15 16 //顯示結果 17 DisplayResults(url, contents); 18 19 //更新總數 20 totalCount += contents.Length; 21 } 22 23 tbResult.Text += $"\r\n Total: {totalCount}, OK!"; 24 }
3.再修改下 btnSwitch_Click
這裡為防止意外地重新輸入操作,先在頂部禁用按鈕,在最終完成時再啟用按鈕。通常,不更改事件處理程式的名稱。 因為事件處理程式不需要返回值,所以返回類型也不需要更改為 Task。
1 /// <summary> 2 /// 非同步點擊事件 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private async void btnSwitch_Click(object sender, RoutedEventArgs e) 7 { 8 btnSwitch.IsEnabled = false; 9 10 //清除文本框所有內容 11 tbResult.Clear(); 12 13 //統計總數 14 await SumSizesAsync(); 15 16 btnSwitch.IsEnabled = true; 17 }
4.其實可以採用 .NET 自帶的 GetByteArrayAsync 非同步方法替換我們自己寫的 GetUrlContentsAsync 非同步方法,之前只是為了演示的需要。
var hc = new HttpClient() { MaxResponseContentBufferSize = 1024000 }; //var contents = await GetUrlContentsAsync(url); var contents = await hc.GetByteArrayAsync(url);
1 /// <summary> 2 /// 非同步統計總數 3 /// </summary> 4 private async Task SumSizesAsync() 5 { 6 7 var hc = new HttpClient() { MaxResponseContentBufferSize = 102400 }; 8 //載入網址 9 var urls = InitUrlInfoes(); 10 11 //位元組總數 12 var totalCount = 0; 13 foreach (var url in urls) 14 { 15 //返回一個 url 內容的位元組數組 16 //var contents = await GetUrlContentsAsync(url); 17 var contents = await hc.GetByteArrayAsync(url); 18 19 //顯示結果 20 DisplayResults(url, contents); 21 22 //更新總數 23 totalCount += contents.Length; 24 } 25 26 tbResult.Text += $"\r\n Total: {totalCount}, OK!"; 27 }修改後的:SumSizesAsync 方法
這時,項目的變換從同步到非同步操作已經完成。
修改後的效果差異:最重要的是,UI 線程不會阻塞下載過程。當 web 資源下載、統計並顯示時,可以移動或調整視窗的大小。如果其中一個網站速度或不響應,可以通過選擇關閉按鈕取消了操作 (右上角的 X)。
同系列的隨筆
【參考引用】微軟官方文檔圖片