認識非同步程式開發設計模型
從VS2012開始引入的新的非同步程式設計的支援-------async/await設計模型
- 之前的當我們支援非同步作業的時候,往往使用多執行緒開解決,我們比較熟悉的就是
- 執行者:Thread,ThreadPool (執行緒和執行緒池,後者有利於資源的有效利用)
- 非同步的設計模型:Begin方法/End方法,Async事件/Completed事件(主要是非同步委託之類的,我在我以前的博文中有寫過專題)
- BackgroundWorker控制項
- Task Parallel Library
雖然今天的重點是.NET4.5的async/await設計模式但是由於很多人對於.NET4.0中的Task仍然還是沒有接觸過,Task也是.NET 4.5 async await的基礎概念之一,值得大家花點時間熟悉,那麼這裡就將他也作為一個附加專題來做一下講解。
附屬專題:.NET4.0多執行緒開發之利器---àTask
到我們在開發SignalR程式的時候,就必須要使用到多執行緒,假設沒有.NET4.5的支援,那麼你可能想到的最簡單方式就是使用Task,它取代了傳統的Thread,TheadPool的寫法,能大幅度的簡化同步邏輯的寫法,頗為便利,下面我們來看幾個典型的範例。
範例1:簡單的開始
Test1()用以另一Thread執行Thread.Sleep()及Console.WriteLine(),效果與ThreadPool.QueueUserWorkItem()相當。
private static void Test1() { //Task可以代替TheadPool.QueueUserWorkItem使用 Task.Factory.StartNew(() => { Thread.Sleep(2000); Console.WriteLine("Done!"); }); Console.WriteLine("Async Run..."); }
StartNew()完會立刻執行下一行,故會先看到Aync Run,1秒後列印出Done。
範例2:等待各作業完成再繼續下一步的應用場境
同時啟動多個作業多工並行(多執行緒並行),但要等待各作業完成再繼續下一步的應用場境傳統方式上可通過WaitHandle、AutoResetEvent、ManualResetEvent等機制實現;Task的寫法相當簡單,建立多個Task物件,再作為Task.WaitAny()或Task.WaitAll()的引數就搞定了!
private static void Test2() { var task1 = Task.Factory.StartNew(() => { Thread.Sleep(3000); Console.WriteLine("Done!(3s)"); }); var task2 = Task.Factory.StartNew(() => { Thread.Sleep(5000); Console.WriteLine("Done!(5s)"); }); //等待任意作業完成後繼續 Task.WaitAny(task1, task1); Console.WriteLine("WaitAny Passed"); //等待所有作業完成後繼續 Task.WaitAll(task1, task2); Console.WriteLine("WaitAll Passed"); }
task1耗時3秒、task2耗時5秒,所以3秒後WaitAny()執行完成、5秒後WaitAll()執行完畢。
範例3:如果要等待多工作業傳回結果
通過StartNew<T>()指定傳回型別建立作業,隨後以Task.Result取值,不用額外Code就能確保多工作業執行完成後才讀取結果繼續執行
private static void Test3() { var task = Task.Factory.StartNew<string>(() => { Thread.Sleep(2000); return "Done!"; }); //使用秒錶計時 Stopwatch sw = new Stopwatch(); sw.Start(); //讀取task.Result時,會等到作業完成傳回值後才繼續 Console.WriteLine("{0}", task.Result); sw.Stop(); //取得task.Result耗時約2秒 Console.WriteLine("Duration: {0:N0}ms", sw.ElapsedMilliseconds); }
實際執行,要花兩秒才能跑完Console.WriteLine("{0}", task.Result),其長度就是Task執行並回傳結果的時間。
範例4:在多工作業完成後接連執行行另一段程式可使用ContinueWith():
private static void Test4() { Task.Factory.StartNew(() => { Thread.Sleep(1000); Console.WriteLine("Done!"); }).ContinueWith(task => { //ContinueWith會等待前面的任務完成才繼續 Console.WriteLine("In ContinueWith"); }); Console.WriteLine("Async Run..."); }
如預期,ContinueWith()裡的程式會在Task完成後才被執行。
範例5:多工作業時各段邏輯便會依順序執行
.ContinueWith()傳回值仍是Task物件,所以我們可以跟jQuery一樣連連看,在ContinueWith()後方再接上另一個ContinueWith(),各段邏輯便會依順序執行。
static void test5() { //ContinueWith()可以串接 Task.Factory.StartNew(() => { Thread.Sleep(2000); Console.WriteLine("{0:mm:ss}-Done", DateTime.Now); }) .ContinueWith(task => { Console.WriteLine("{0:mm:ss}-ContinueWith 1", DateTime.Now); Thread.Sleep(2000); }) .ContinueWith(task => { Console.WriteLine("{0:mm:ss}-ContinueWith 2", DateTime.Now); }); Console.WriteLine("{0:mm:ss}-Async Run...", DateTime.Now); }
Task耗時兩秒,第一個ContinueWith()耗時2秒,最後一個ContinueWith()繼續在4秒後執行。
範例6:Task有監控狀態的機制
ContinueWith()中的Action<Task>都會有一個輸入引數,用於以得知前一Task的執行狀態,有IsCompleted, IsCanceled, IsFaulted幾個屬性可用。要取消執行,得藉助CancellationTokenSource及其所屬CancellationToken型別,做法是在Task中持續呼叫CancellationToken.ThrowIfCancellationRequested(),一旦外部呼叫CancellationTokenSource.Cancel(),便會觸發OperationCanceledException,Task有監控此異常狀況的機制,將結束作業執行後續ContinueWith(),並指定Task.IsCanceled為True;而當Task程式傳送Exception,也會結束觸發ContinueWith (),此時Task.IsFaulted為True,ContinueWith()中可通過Task.Exception.InnerExceptions取得錯誤細節。以下程式同時可測試Task正常、取消及錯誤三種情景,使用者通過輸入1,2或3來決定要測試哪一種。在Task外先宣告一個CancellationTokenSource型別,將其中的Token屬性當成StartNew()的第二項引數,而Task中則保留最初的五秒可以取消,方法是每隔一秒呼叫一次CancellationToken.ThrowIfCancellationRequested(),當程式外部呼叫CancellationTokenSource.Cancel(),Task就會結束。5秒後若未取消,再依使用者決定的測試情境return結果或是丟擲Exception。ContinueWith()則會檢查IsCanceled, IsFaulted等標識,並輸出結果。
private static void Test6() { CancellationTokenSource cts = new CancellationTokenSource(); CancellationToken cancelToken = cts.Token;//獲取與此CancellationTokenSource關聯的CancellationToken Console.Write("Test Option 1, 2 or 3 (1-Complete / 2-Cancel / 3-Fault) : "); var key = Console.ReadKey(); Console.WriteLine(); Task.Factory.StartNew<string>(() => { //保留5秒檢測是否要Cancel for (var i = 0; i < 5; i++) { Thread.Sleep(1000); //如cancelToken.IsCancellationRequested //引發OperationCanceledException cancelToken.ThrowIfCancellationRequested(); } switch (key.Key) { case ConsoleKey.D1: //選1時 return "OK"; case ConsoleKey.D3: //選3時 throw new ApplicationException("MyFaultException"); } return "Unknown Input"; }, cancelToken).ContinueWith(task => { Console.WriteLine("IsCompleted: {0} IsCanceled: {1} IsFaulted: {2}", task.IsCompleted, task.IsCanceled, task.IsFaulted); if (task.IsCanceled) { Console.WriteLine("Canceled!"); } else if (task.IsFaulted) { Console.WriteLine("Faulted!"); foreach (Exception e in task.Exception.InnerExceptions) { Console.WriteLine("Error: {0}", e.Message); } } else if (task.IsCompleted) { Console.WriteLine("Completed! Result={0}", task.Result); } }); Console.WriteLine("Async Run..."); //如果要測Cancel,2秒後觸發CancellationTokenSource.Cancel if (key.Key == ConsoleKey.D2) { Thread.Sleep(2000); cts.Cancel(); } }
Task能做的事,過去使用Thread/ThreadPool配合Event、WaitHandle一樣能辦到,但使用Task能以比較簡潔的語法完成相同工作,使用.NET 4.0開發多執行緒時可多加利用。
到這裡,我們繼續回到原本的.NET4.5中,首先我們設計幾種非同步作業新舊寫法法進行對比
利用WebClient類別下載網頁內容
1.使用Async/Complete設計模式
private static void DownLoadWebPageSourceCode_Old() { WebClient wc = new WebClient(); wc.DownloadStringCompleted += CompletedHandler; wc.DownloadStringAsync(new Uri("http://www.cnblogs.com/rohelm")); while (wc.IsBusy) { Console.WriteLine("還沒下完,我喝一回茶!"); } } private static void CompletedHandler(object sender, DownloadStringCompletedEventArgs e) { Console.WriteLine(e.Result); }
執行效果如下:
2.使用新的async/await設計模式
private static async void DownLoadWebPageSourceCode_New() { WebClient wc = new WebClient(); Console.WriteLine(await wc.DownloadStringTaskAsync("http://www.cnblogs.com/rohelm")); }
而它的內部實現機制實際上是我們前面的附加專題中提到的Task,我們來檢視下這個方法的原始碼:
[ComVisible(false), HostProtection(SecurityAction.LinkDemand, ExternalThreading=true)] public Task<string> DownloadStringTaskAsync(string address) { return this.DownloadStringTaskAsync(this.GetUri(address)); } [ComVisible(false), HostProtection(SecurityAction.LinkDemand, ExternalThreading=true)] public Task<string> DownloadStringTaskAsync(Uri address) { TaskCompletionSource<string> tcs = new TaskCompletionSource<string>(address); DownloadStringCompletedEventHandler handler = null; handler = delegate (object sender, DownloadStringCompletedEventArgs e) { this.HandleCompletion<DownloadStringCompletedEventArgs, DownloadStringCompletedEventHandler, string>(tcs, e, args => args.Result, handler, delegate (WebClient webClient, DownloadStringCompletedEventHandler completion) { webClient.DownloadStringCompleted -= completion; }); }; this.DownloadStringCompleted += handler; try { this.DownloadStringAsync(address, tcs); } catch { this.DownloadStringCompleted -= handler; throw; } return tcs.Task; }
3.取消非同步的方式
由於上面我們已經說過它的內部本質還是Task所以它的,取消該非同步作業依舊藉助CancellationTokenSource及其所屬CancellationToken型別
private static async Task TryTask() { CancellationTokenSource cts = new CancellationTokenSource(); cts.CancelAfter(TimeSpan.FromSeconds(1)); Task<string> task = Task.Run(() => PirntWords("Hello,Wrold!", cts.Token), cts.Token); Console.WriteLine(task.Result); await task; } private static string PirntWords(string input, CancellationToken token) { for (int i = 0; i < 20000000; i++) { Console.WriteLine(input); token.ThrowIfCancellationRequested(); } return input; }
網頁呼叫用多個Web服務/WCF服務/Http服務模型
public async Task<ActionResult> DoAsync() { ServiceClient1 client1 = new ServiceClient1(); ServiceClient2 client2 = new ServiceClient2(); var task1 = client1.GetDataAsync(); var task2 = client2.GetDataAsync(); await Task.WhenAll(task1,task2); return View("View名稱",new DataModel(task1.Result,task2.Rusult)); }
是不是發現非常的方便,實用啊!
未完待續....
後續內容:
對WebAPI和WCF的進行一個簡單比較,探討WebAPI的機制,功能,架構,WinFrom Client/WebService Client大資料上傳...備註:本文章版權的沒有,歸.NET使用者共有。