帶着問題去思考!大家好
簡介
微軟提供的最新的異步編程基礎設施。它允許我們以模塊化的方式設計程序,來組合不同的異步操作。
1:遺憾的是,當閱讀此類程序時仍然非常難理解程序實際執行順序。很多大型的程序中將會有許多相互依賴的任務和後續操作,處理異常的後續操作,並且它們都會出現在程序代碼中的不同地方,因此瞭解程序的先後執行次序很難。
2:能夠接觸用戶界面控制的每個異步任務是否得到了正確的同步上下文。程序只允許通過UI線程使用這些控制器,否則將會得到多線程的訪問異常。說到異常,我們不得不使用單獨的後續操作任務來處理在之前的異步操作中發生的錯誤。這又導致分散在代碼的不同部分的複雜的處理錯誤的代碼。邏輯無法相互關聯。
C#5.0引入了新的語言特性。異步函數,它是TPL之上的更高級別的抽象。真正簡化了異步編程。
須知:
- 創建一個異步函數,首先需要用async關鍵字標註一個方法。
- 異步函數必須返回Task或Task<T>類型,可以使用async void 方法,但是更推薦使用async task方法,
- 使用async標註的方法內部,可以使用await操作符,該操作符可與TPL的任務一起工作,並獲取該任務中異步操作的結果。
- 異步函數在其代碼中至少擁有一個await操作符。(沒有則只是警告)
- 不能在catch,finally,lock或unsafe代碼塊中使用await操作符。
- 不允許對任何異步函數使用ref或out參數。
如果程序中有兩個連續await操作符,他們是順序運行的,第一個完成後第二個纔會開始運行
使用await操作符獲取異步任務結果
/// <summary> /// 使用await獲取結果 /// </summary> /// <returns></returns> public static Task AsynchronyWithTPL() { Task<string> t = GetInfoAsync("Task 1"); Task t2 = t.ContinueWith(task => Console.WriteLine(t.Result), TaskContinuationOptions.NotOnFaulted); Task t3 = t.ContinueWith(task => Console.WriteLine(t.Exception.InnerException),TaskContinuationOptions.OnlyOnFaulted); return Task.WhenAll(t2, t3); } async static Task AsynchronyWithAwait() { try { string result = await GetInfoAsync("Task 2"); Console.WriteLine(result); } catch (Exception ex) { Console.WriteLine(ex); } } private async static Task<string> GetInfoAsync(string name) { await Task.Delay(TimeSpan.FromSeconds(2));//創建在指定時間間隔後完成的任務。 return string.Format("Task {0} is running on a thread id {1}.Is thread pool thread :{2}",name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); } static void Main(string[] args) { Task t = AsynchronyWithTPL(); t.Wait(); t = AsynchronyWithAwait(); t.Wait(); }
這裏需要注意的是Task.Wait和Task.Result方法,如果不是百分百知道代碼是幹什麼的,很容易造成死鎖。
對連續的異步任務使用await操作符
/// <summary> /// 對連續的異步任務使用await /// </summary> /// <returns></returns> public static Task AsynchronyWithTPL() { var containerTask = new Task(() => { Task<string> t = GetInfoAsync("TPL 1"); t.ContinueWith(task => { Console.WriteLine(t.Result); Task<string> t2 = GetInfoAsync("TPL 2"); t2.ContinueWith(innerTask => Console.WriteLine(innerTask.Result), TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent); t2.ContinueWith(innerTask => Console.WriteLine(innerTask.Exception.InnerException), TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent); }, TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent); t.ContinueWith(task => Console.WriteLine(t.Exception.InnerException), TaskContinuationOptions.NotOnFaulted | TaskContinuationOptions.AttachedToParent); }); containerTask.Start(); return containerTask; } async static Task AsynchronyWithAwait() { try { string result = await GetInfoAsync("Async 1"); Console.WriteLine(result); result = await GetInfoAsync("Async 2"); Console.WriteLine(result); } catch (Exception ex) { Console.WriteLine(ex); } } private async static Task<string> GetInfoAsync(string name) { Console.WriteLine("任務 {0} 開始!",name); await Task.Delay(TimeSpan.FromSeconds(2));//創建在指定時間間隔後完成的任務。 if (name == "TPL 2") throw new Exception("Boom"); return string.Format("Task {0} is running on a thread id {1}.Is thread pool thread :{2}", name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); } static void Main(string[] args) { Task t = AsynchronyWithTPL(); t.Wait(); t = AsynchronyWithAwait(); t.Wait(); }
同樣運行兩個異步操作,首先AsynchronyWithAwait方法將講,這裏使用了兩個await聲明,最重要一點是該代碼是順序執行,閱讀代碼很清晰,但是該程序如何是異步程序呢?首先,他不總是異步的,當使用await時如果一個任務已經完成,我們會異步地得到該任務結果。否則,當代碼中看到await聲明時,通常的行爲是方法執行到該await代碼行時將立即返回,並且剩下的的代碼將會在一個後續操作任務中運行,因此等待操作結果時並沒有阻塞程序執行。這是一個異步調用。
異步並不總是意味着並行執行。
對並行執行的異步任務使用await操作符
async static Task AsynchronousProccessing() { Task<string> t1 = GetInfoAsync("task1",3); Task<string> t2 = GetInfoAsync("task2", 5); string[] results = await Task.WhenAll(t1, t2); foreach (var result in results) { Console.WriteLine(result); } } private async static Task<string> GetInfoAsync(string name,int seconds) { await Task.Run(() => { TimeSpan.FromSeconds(seconds); }); return string.Format("Task {0} is running on a thread id {1},Is thread pool thread {2}",name, Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread); } static void Main(string[] args) { Task t = AsynchronousProccessing(); t.Wait(); }
這裏定義了兩個異步任務,分別執行了3秒和5秒。然後使用了Task.WhenAll輔助方法創建另一個任務,該任務只有在所有底層任務完成後才運行,之後我們等待該組合任務的結果。這裏兩個任務似乎是被線程池中的同一個線程執行的。
兩個任務被不同的工作線程執行,不同之處在於Task.Delay在幕後使用了一個計時器,從線程池中獲取工作線程,它將等待Task.Delay方法返回結果。然後,Task.Delay方法啓動計時器並指向一塊代碼,該代碼會在計時器時間到了Task.Delay方法中指定的秒數後被調用。之後即將工作線程返回到線程池中。當計時器事件運行時,我們又從線程池中任意獲取一個可用的工作線程(可能就是運行一個任務時使用的線程
當使用Task.Run方法時,從線程池中獲取一個工作線程並將其阻塞幾秒,具體秒數由Thread.Sleep方法提供。然後獲取了第二個。 工作線程並且將其阻塞。在這種場景下,我們消費了兩個工作線程,而它們絕對什麼事沒做,因爲在它們等待時不能執行任何其他操作。
處理異步操作中的異常
async static Task AsynchronousProcessing() { Console.WriteLine("1.Single exception"); try { string restult = await GetInfoAsync("Task 1", 2); Console.WriteLine(restult); } catch (Exception ex) { Console.WriteLine("Exception details:{0}", ex); } Console.WriteLine(); Console.WriteLine("2.Multiple exception"); Task<string> t1 = GetInfoAsync("Task 1", 3); Task<string> t2 = GetInfoAsync("Task 2", 2); try { string[] result = await Task.WhenAll(t1, t2); Console.WriteLine(result.Length); } catch (Exception ex) { Console.WriteLine("Exception details:{0}", ex); } Console.WriteLine(); Console.WriteLine("2. Multiple execption with AggregateException"); t1 = GetInfoAsync("Task 1", 3); t2 = GetInfoAsync("Task 2", 2); Task<string[]> t3 = Task.WhenAll(t1,t2); try { string[] results = await t3; Console.WriteLine(results.Length); } catch (Exception ex) { var ae = t3.Exception.Flatten(); var exceptions = ae.InnerExceptions; Console.WriteLine("Exceptions caught:{0}", exceptions.Count); foreach (var e in exceptions) { Console.WriteLine("Exception details :{0}",e); Console.WriteLine(); } } } private async static Task<string> GetInfoAsync(string name, int seconds) { await Task.Delay(TimeSpan.FromSeconds(seconds)); throw new Exception(string.Format("Boom from {0} ", name)); } static void Main(string[] args) { Task t = AsynchronousProcessing(); t.Wait(); }
這裏實現了三個場景來展示在C#中使用async和await時關於錯誤處理的最常見情況,一種情況最簡單,與同步代碼幾乎一樣,使用try/catch聲明即可獲取異常細節。一種很常見的錯誤是對一個以上的異步操作使用await時還使用以上方式。如果跟第一種處理的話,則只能從底層的AggregateException對象中得到第一個異常。爲了收集所有異常信息,可以使用await任務的Exception屬性,在第三種情況中,我們使用AggregateException的Flatten方法將層級異常放入一個列表,並且從中提取出所有的底層異常。