1. 演進過程
本文檔主要記錄.net平臺下異步編程不同時期的不同實現方案,.net平臺異步編程經歷了以下幾次演變:
- Asynchronous Programming Model(APM):這種模式又被成爲IAsyncResult模式,在.net1.0時提出,在同步方法中通過調用BeginXXXX和EndXXXX開頭的方法對實現異步操作,此模式需要分配和回收IAsyncResult對象消耗資源降低效率,且不支持取消和沒有提供進度報告的功能,微軟不推薦使用。
- Event-based Asynchronous Pattern(EAP):它是基於事件模式的異步實現,在.net2.0時提出,這種模式具有一個或多個以Async爲後綴的方法和Completed事件,它們都支持異步方法的取消、進度報告和報告結果,且其基於APM模式,此模式效率雖高,但.net中並不是所有類都支持,且業務複雜時就很難控制,微軟不推薦使用。
- 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(),微軟推薦使用的。
- 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. 參考信息
- 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.
- https://www.cnblogs.com/fanfan-90/p/12006157.html
- https://www.cnblogs.com/zhili/archive/2013/05/13/TAP.html
- https://www.cnblogs.com/jonins/p/9558275.html