[.NET] 怎樣使用 async & await 一步步將同步代碼轉換爲異步編程

怎樣使用 async & await 一步步將同步代碼轉換爲異步編程

【博主】反骨仔    【出處】http://www.cnblogs.com/liqingwen/p/6079707.html   html

  上次,博主經過《利用 async & await 的異步編程》該篇點睛之做介紹了 async & await 的基本用法及異步的控制流和一些其它的東西。git

  今天,博主打算從建立一個普通的 WPF 應用程序開始,看看如何將它逐步轉換成一個異步的解決方案web

 

目錄

 

介紹

  這裏經過一個普通的 WPF 程序進行講解:編程

  只是一個文本框和一個按鈕,左邊文本框的內容爲點擊右鍵按鈕時所產生的結果。數組

 

添加引用

  demo 可能須要用到的部分 using 指令:promise

using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;

 

先建立一個同步的 WPF

  1.這是右邊點擊按鈕的事件:app

 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 信息列表;async

    ② GetUrlContents:獲取網址內容;ide

    ③ 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

 

測試結果圖

   界面上的內容顯示須要花費必定的時間(多是數秒)。

  當你點擊啓動的同時,也就是下載 url 內容的時候,即等待資源的一個過程,此時,UI 線程會進行阻塞。由於在等待資源的這一個期間,咱們沒法對 UI 進行其餘操做,如:移動、最大、最小和關閉窗口等操做。這樣會令用戶很是反感,特別是時間一長的時候,就會出現界面還沒有響應,而且下載時候(網站沒有響應或響應時間過長),也沒法凸顯站點失敗的有效信息。

  在 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),不再須要打開任務管理器進行關閉該進程了。

      Demo 下載

 

同系列的隨筆

 


【參考】https://docs.microsoft.com/en-us/dotnet/articles/csharp/programming-guide/concepts/async/walkthrough-accessing-the-web-by-using-async-and-await

【參考引用】微軟官方文檔

相關文章
相關標籤/搜索