.NET&C#異步編程

1.  演進過程

          本文檔主要記錄.net平臺下異步編程不同時期的不同實現方案,.net平臺異步編程經歷了以下幾次演變:

  1. Asynchronous Programming Model(APM):這種模式又被成爲IAsyncResult模式,在.net1.0時提出,在同步方法中通過調用BeginXXXX和EndXXXX開頭的方法對實現異步操作,此模式需要分配和回收IAsyncResult對象消耗資源降低效率,且不支持取消和沒有提供進度報告的功能,微軟不推薦使用。
  2. Event-based Asynchronous Pattern(EAP):它是基於事件模式的異步實現,在.net2.0時提出,這種模式具有一個或多個以Async爲後綴的方法和Completed事件,它們都支持異步方法的取消、進度報告和報告結果,且其基於APM模式,此模式效率雖高,但.net中並不是所有類都支持,且業務複雜時就很難控制,微軟不推薦使用。
  3. Task-based Asynchronous Pattern(TAP:task):它是基於任務模式的異步實現,在.net4.0時提出,這種模式有四種方法創建Task,1.Task.Factory.StartNew()2.(new Task(()=>{ //TODO })).Start()3.Task.Run()是.net4.5增加4.Task.FromResult(),微軟推薦使用的。
  4. Task-based Asynchronous Pattern(TAP:async/await):它是基於任務模式的異步實現,在.net4.5時提出,它與第三種實現實質上相等,使用這兩個關鍵字會使代碼看起來與同步代碼相當和簡潔,進一步摒棄掉異步編程的複雜結構,微軟極力推薦使用的異步編程模式。

2.  模式:APM和EAP

2.1.  APM

本人在WCF時期應用APM模式調用服務使用最廣泛,現在除了UI交互外很少使用APM模式,以下示例僅爲展示APM編碼模式

 1 public void Test(){
 2     var urlStr="http://www.test.com/test/testAPM";
 3     var request=HttpWebRequest.Create(url);
 4     request.BeginGetResponse(AsyncCallbackImpl,request);//發起異步請求
 5 }
 6 public void AsyncCallbackImpl(IAsyncResult ar){
 7     var request=ar.AsyncState as HttpWebRequest;
 8     var response=request.EndGetResponse(ar);//結束異步請求
 9     using(var stream=response.GetResponseStream()){
10         var sbuilder=new StringBuilder();
11         sbuilder.AppendLine($"當前線程Id:{Thread.CurrentThread.ManagedThreadId}");
12         var reader=new StreamReader(stream);
13         sbuilder.AppendLine(reader.ReadLine());
14         Console.WriteLine(sbuilder.ToString());
15     }
16 }

 

2.2.  EAP

在大多數數據庫連接驅動中使用,本人在即時通信軟件中使用過,以下示例僅爲展示EAP編碼模式

2.2.1.  Demo:WebClient

1 public void Test(){
2     var wc=new WebClient();
3     wc.DownloadStringCompleted+=wc_DownloadStringCompleted;
4     wc.DownloadStringAsync(new Uri("http://www.test.com/test/testEAP"));
5 }
6 public void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e){
7     Console.WriteLine(e.Result);
8 }

 

2.2.2.  Demo:BackgroundWorker

 1 public void Test(){
 2     var bgworker=new BackgroundWorker();
 3     bgworker.DoWork+=bgworker_DoWork;
 4     bgworker.RunWorkerCompleted+=bgworker_RunWorkerCompleted;
 5     bgworker.RunWorkerAsync(null);//參數會被傳遞到DoWork事件訂閱者方法中,而內部實際調用了BeginInvoke()方法
 6 }
 7 public void bgworker_DoWorker(object sender,DoWorkEventArgs e){
 8     Console.WriteLine("dowork");
 9 }
10 public void bgworker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e){
11     Console.WriteLine("dowork completed");
12 }

3.  模式:TAP

3.1.  常用對象和方法

由於微軟推薦使用TAP方式編碼,所以本節內容是本篇文章的重點。其實TAP主要使用了以下對象和方法實現異步編程:

1)      Task<Result>:異步任務

2)      Task<Result>.ContinueWith(Action):延續任務,指定任務執行完成後延續的操作

3)      Task.Run():創建異步任務

4)      Task.WhenAll():在所有傳入的任務都完成時才返回Task

5)      Task.WhenAny():在傳入的任務其中一個完成就會返回Task

6)      Task.Delay():異步延時等待,示例Task.Delay(2000).Wait()

7)      Task.Yield():進入異步方法後,在await之前,如果存在耗時的同步代碼,且你想讓這部分代碼也異步執行,那麼你就可以在進入異步方法之後的第一行添加await Task.Yield()代碼了,因爲它會強制將當前方法轉爲異步執行。

3.2.  關鍵字:async/await

1)      使用async關鍵字標記的方法成爲異步方法,異步方法通常包含await關鍵字的一個或多個實例,如果異步方法中未使用await關鍵字標識對象方法,那麼異步方法會視爲同步方法。

2)      await關鍵字無法等待具有void返回類型的異步方法,並且void返回方法的調用方捕獲不到異步方法拋出的任何異常。

3)      異步方法無法聲明in、ref或out參數,但可以調用包含此類參數的方法。

3.3.  使用示例

3.3.1.  同步方法

 1 public void Test(){
 2     Console.WriteLine($"頭部已執行,當前主線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 3     var result = SayHi("abc");
 4     Console.WriteLine(result);
 5     Console.WriteLine($"尾部已執行,當前主線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 6     Console.ReadKey();
 7 }
 8 public string SayHi(string name){
 9     Task.Delay(2000).Wait();//異步等待2s
10     Console.WriteLine($"SayHi執行,當前線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
11     return $"Hello,{name}";
12 }

3.3.2.  異步實現

 1 public void Test(){
 2     Console.WriteLine($"頭部已執行,當前主線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 3     var result = SayHiAsync("abc").Result;
 4     Console.WriteLine(result);
 5     Console.WriteLine($"尾部已執行,當前主線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 6     Console.ReadKey();
 7 }
 8 public Task<string> SayHiAsync(string name){
 9     return Task.Run<string>(() => { return SayHi(name); });
10 }
11 public string SayHi(string name){
12     Task.Delay(2000).Wait();//異步等待2s
13     Console.WriteLine($"SayHi執行,當前線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
14     return $"Hello,{name}";
15 }

3.3.3.  延續任務

 1 public void Test(){
 2     Console.WriteLine($"頭部已執行,當前主線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 3     var task = SayHiAsync("abc");
 4     task.ContinueWith(t=>{
 5         Console.WriteLine($"延續執行,當前線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 6         var result=t.Result;
 7         Console.WriteLine(result);
 8     });
 9     Console.WriteLine($"尾部已執行,當前主線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
10         Console.ReadKey();
11 }
12 public Task<string> SayHiAsync(string name){
13     return Task.Run<string>(() => { return SayHi(name); });
14 }
15 public string SayHi(string name){
16     Task.Delay(2000).Wait();//異步等待2s
17     Console.WriteLine($"SayHi執行,當前線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
18     return $"Hello,{name}";
19 }

3.3.4.  async/await重構

 1 public void Test(){
 2     Console.WriteLine($"頭部已執行,當前主線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 3     SayHiKeyPair("abc");
 4     Console.WriteLine($"尾部已執行,當前主線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 5     Console.ReadKey();
 6 }
 7 public async void SayHiKeyPair(string name){
 8     Console.WriteLine($"異步調用頭部執行,當前線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
 9     var result = await SayHiAsync(name);
10     Console.WriteLine($"異步調用尾部執行,當前線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
11     Console.WriteLine(result);
12 }
13 public Task<string> SayHiAsync(string name){
14     return Task.Run<string>(() => { return SayHi(name); });
15 }
16 public string SayHi(string name){
17     Task.Delay(2000).Wait();//異步等待2s
18     Console.WriteLine($"SayHi執行,當前線程Id爲:{Thread.CurrentThread.ManagedThreadId}");
19     return $"Hello,{name}";
20 }

3.4.  運行流程

爲了避免繁雜的概念,簡單明瞭的概述爲:XXXXAsync方法返回一個Task<Result>,await Task<Result>處等待異步結果,在它們中間可以執行一些與異步任務無關的邏輯。

4.  轉爲:TAP

4.1.  APM轉化爲TAP

現在將第二節中的APM實現轉爲TAP實現,主要藉助Task.Factory.FromAsync方法

 1 public void APMtoTAP(){
 2     var urlStr="http://www.test.com/test/testAPM";
 3     var request=HttpWebRequest.Create(url);
 4         Task.Factory.FromAsync<HttpWebResponse>(request.BeginGetResponse,request.EndGetResponse,null,TaskCreationOptions.None)
 5             .ContinueWith(t=>{
 6                 var response=null;
 7                 try{
 8                     response=t.Result;
 9                     using(var stream=response.GetResponseStream()){
10                         var sbuilder=new StringBuilder();
11                         sbuilder.AppendLine($"當前線程Id:{Thread.CurrentThread.ManagedThreadId}");
12                         var reader=new StreamReader(stream);
13                         sbuilder.AppendLine(reader.ReadLine());
14                         Console.WriteLine(sbuilder.ToString());
15                     }
16                 }catch(AggregateException ex){
17                     if (ex.GetBaseException() is WebException){
18                         Console.WriteLine($"異常發生,異常信息爲:{ex.GetBaseException().Message}");
19                     }else{
20                         throw;
21                     }
22                 }finally{
23                     if(response!=null){
24                         response.Close();
25                     }
26                 }
27             });
28 }

4.2.  EAP轉化爲TAP

 1 public void Test(){
 2     var wc=new WebClient()// WebClient類支持基於事件的異步模式(EAP)
 3     var tcs = new TaskCompletionSource<string>();//創建TaskCompletionSource和它底層的Task對象
 4 
 5     wc.DownloadStringCompleted+=(sender,e)=>{//一個string下載好之後,WebClient對象會應發DownloadStringCompleted事件
 6         if(e.Error != null){
 7             tcs.TrySetException(e.Error);//試圖將基礎Tasks.Task<TResult>轉換爲Tasks.TaskStatus.Faulted狀態
 8         }else if(e.Cancelled){
 9             tcs.TrySetCanceled();//試圖將基礎Tasks.Task<TResult>轉換爲Tasks.TaskStatus.Canceled狀態
10         }else{
11             tcs.TrySetResult(e.Result);//試圖將基礎Tasks.Task<TResult>轉換爲TaskStatus.RanToCompletion狀態。
12         }
13 };
14     tsc.Task.ContinueWith(t=>{//爲了讓下面的任務在GUI線程上執行,必須標記爲TaskContinuationOptions.ExecuteSynchronously
15         if(t.IsCanceled){
16             Console.WriteLine("操作已被取消");
17         }else if(t.IsFaulted){
18             Console.WriteLine("異常發生,異常信息爲:" + t.Exception.GetBaseException().Message);
19         }else{
20             Console.WriteLine(String.Format("操作已完成,結果爲:{0}", t.Result));
21         }
22     },TaskContinuationOptions.ExecuteSynchronously);
23         
24     wc.DownloadStringAsync(new Uri("http://www.test.com/test/testEAP"));
25 }

5.  總結

在設計異步編程時,要確定異步操作是I/O-Bound(因I/O阻塞,又稱爲I/O密集型),還是CPU-Bound(因CPU阻塞,又稱爲計算密集型),從而更好的選擇方式方法。計算密集型並不是任務越多越好,如果任務數量超過CPU的核心數,那麼花費在任務切換上的時間就越多,CPU的執行效率就越低。I/O密集型由於任務主要在硬盤讀寫和網絡讀寫上,所以CPU就可以處理非常多的任務。

之所以有這篇文章,因爲沒有搜到類似本文,僅需一篇文章記錄儘量全面的文章,所以就做了回搬運工,整理彙總一下。

6.  參考信息

  1. https://docs.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/#:~:text=For%20more%20information%2C%20see%20Task-based%20Asynchronous%20Pattern%20%28TAP%29.,event%20handler%20delegate%20types%2C%20and%20EventArg%20-derived%20types.
  2. https://www.cnblogs.com/fanfan-90/p/12006157.html
  3. https://www.cnblogs.com/zhili/archive/2013/05/13/TAP.html
  4. https://www.cnblogs.com/jonins/p/9558275.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章