避免async void
非同步方法返回型別有3種,void,Task和Task<T>,void儘量不要使用。
原理剖析:
使用async void標記的方法有不同的錯誤處理語義。async Task或async Task方法丟擲異常時,異常會被捕獲並放到Task物件上。然而,標記為async void的方法沒有Task物件,所以async void方法丟擲的任何異常都會直接放到SynchronizationContext(非同步上下文)上,它是在async void方法開始的時候啟用的。下面是一個例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//async void 方法不能被捕獲的異常 private async void ThrowExceptionAsync() { throw new InvalidOperationException(); } public void AsyncVoidExceptions_CannotBeCaughtByCatch() { try { ThrowExceptionAsync(); } catch (Exception ) { //異常不會被捕獲 throw; } } |
async void有不同的組成語法。返回Task或Task的async方法可以使用await Task.WhenAny或Task.WhenAll等輕易組合。而返回void的async方法沒有提供簡單的方式來通知它們已經完成的呼叫程式碼。啟用若干個async void方法很容易,但不容易決定它們什麼時候完成。async void方法開始和完成時會通知它們的SynchronizationContext,但是自定義的SynchronizationContext對於常規應用程式碼是一個複雜的解決方案。
async void方法測試很困難。由於錯誤處理和組合的差異,編寫呼叫async void方法的單元測試很困難。
很明顯,async void方法與async Task方法相比有很多劣勢,但在一個特殊場合很有用,那就是非同步的事件控制程式碼。它們直接將異常丟擲到SynchronizationContext,這與同步的事件控制程式碼表現很相似。同步的事件控制程式碼通常是私有的,因此它們不能被組合或者直接測試。我想採取的方法是在非同步事件控制程式碼中最小化程式碼,比如,讓它await一個包含實際邏輯的async Task方法,程式碼如下:
1 2 3 4 5 6 7 8 9 |
private async void button1_Click(object sender, EventArgs e) { await Button1ClickAsync(); } public async Task Button1ClickAsync() { //處理非同步工作 await Task.Delay(1000); } |
總之,對於async Task和async void,你應該更喜歡前者。async Task方法更容易錯誤處理,組合和測試。對於非同步的事件控制程式碼異常,必須返回void。
一直使用async
這句話的意思是,不要不經過認真考慮就混合同步和非同步程式碼。特別地,在非同步程式碼上使用Task.Wait或Task.Result是一個餿主意。
下面是一個簡單的例子:一個方法阻塞了非同步方法的結果。在控制檯程式中會工作的很好,但是從GUI或者ASP.Net上下文中呼叫的時候就會死鎖。死鎖的實際原因是當呼叫Task.Wait的時候進一步開啟了呼叫棧。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
//阻塞非同步程式碼時的一個常見死鎖問題 public static class DeadlockDemo { private static async Task DelayAsync() { await Task.Delay(1000); } // 呼叫 GUI 或 ASP.NET 上下文的時候會造成死鎖 public static void Test() { // 開始延遲. var delayTask = DelayAsync(); // 等待延遲 delayTask.Wait(); } } |
造成這種死鎖的根本原因是等待處理上下文的方式。預設情況下,當一個未完成的Task處於被等待狀態時,當前上下文會被捕獲並且當此任務完成時恢復該方法。這個上下文如果不為null就是當前的SynchronizationContext,在這種情況下,它是當前的TaskScheduler(任務排程者)。GUI 和ASP.NET應用有一個SynchronizationContext,它只允許一次執行一大塊程式碼。當await完成時,它嘗試在捕獲的上下文內執行非同步方法的剩餘部分。但是該上下文已經有一個執行緒了,它在(同步地)等待這個async方法的完成。它們每一個都在等待另一個,造成了死鎖。
注意控制檯程式不會造成這種死鎖。它們有個執行緒池SynchronizationContext而沒有一次執行一大坨程式碼的SynchronizationContext,因此當await完成時,它線上程池執行緒上排程該async方法的剩餘部分。該方法可以完成,它完成了返回task,並沒有死鎖。
總之,應該避免混合async和阻塞的程式碼。這樣做的話會造成死鎖,更復雜的錯誤處理和上下文執行緒不可預測的阻塞。
配置上下文
這裡稍加補充如下:
除了效能方面之外,ConfigureAwait還有另一個重要的方面:它可以避免死鎖。在“一直使用async”的程式碼示例中,再次思考一下:如果你在DelayAsync程式碼行新增“ConfigureAwait(false)”,那麼死鎖就會避免。這次,當await完成時,它嘗試線上程池上下文內執行async方法的剩餘部分。該方法可以完成,完成後返回task,並且沒有死鎖。這項技術對於逐漸將應用從同步轉為非同步特別有用。
建議將ConfigureAwait用在方法中的每個await之後。只有當未完成的Task被等待時,才會喚起上下文被捕獲;如果Task已經完成了,那麼上下文不會被捕獲。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
async Task MyMethodAsync() { //這裡的程式碼執行在原始 context. await Task.FromResult(1); //這裡的程式碼執行在原始 context. await Task.FromResult(1).ConfigureAwait(continueOnCapturedContext: false); // 這裡的程式碼執行在原始 context. var random = new Random(); int delay = random.Next(2); // delay是 0 or 1 await Task.Delay(delay).ConfigureAwait(continueOnCapturedContext: false); // 這裡的程式碼不確定是否執行在原始 context. } |
每個非同步方法都有自己的上下文,因此如果一個非同步方法呼叫另一個非同步方法,那麼它們的上下文是獨立的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
private async Task HandleClickAsync() { // 這裡可以使用ConfigureAwait await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false); } private async void button1_Click(object sender, EventArgs e) { button1.Enabled = false; try { // 這裡不能使用 ConfigureAwait await HandleClickAsync(); } finally { // 返回到這個方法的原始上下文 button1.Enabled = true; } } |
今天就寫到這裡吧,還有很多很高階的用法,需要自己好好研究一下才能分享出來,希望大家多多支援!