如何用 C# 寫一個好的非同步方法

語言: CN / TW / HK

用C#設計一個好的非同步方法:

  • 它應該有儘量少的引數,甚至不要引數。如果可能的話一定要避免refout引數。
  • 如果有意義的話,他應該有一個返回型別,他能真正的表達方法程式碼的結果,而不是像 C++ 那種成功標識。
  • 它應該有一個可以解釋自己行為的命名,而不依賴於額外的符號或註釋。

使用Async void是一大禁忌:

Async void 方法中丟擲的異常無法通過外面的方法捕獲

async Taskasync Task<T> 方法中丟擲異常,這個異常會被捕獲並且放置到 Task 物件中。而 Async void 中沒有 Task 物件,所以 Async void 丟擲任何異常都將直接在啟動非同步 void 方法時處於活動狀態的 SynchronizationContext 上引發。

SynchronizationContext

SynchronizationContext 可以使一個執行緒與另一個執行緒進行通訊。假設你有兩個執行緒,Thead1 和 Thread2。Thread1 做某些事情,然後它想在 Thread2 裡面執行一些程式碼。一個可以實現的方式是:請求 Thread 得到 SynchronizationContext 這個物件,把它給 Thread1,然後 Thread1 可以呼叫 SynchronizationContext 的 send 方法在 Thread2 裡面執行程式碼。

不是每一個執行緒都有一個 SynchronizationContext 物件。一個總是有 SynchronizationContext 物件的是 UI 執行緒。UI 執行緒中的控制元件被建立的時候會把 SynchronizationContext 物件放到這個執行緒中。 SynchronizationContext.Current 物件不是一個 AppDomain 一個例項的,而是每個執行緒一個例項。這就意味著兩個執行緒在呼叫Synchronization.Current時將會擁有他們自己的 SynchronizationContext 物件例項。Context 上下文儲存線上程data store(不是在 appDomain 的全域性記憶體空間)。

SynchronizationContext 中有 send、post 靜態方法。

參考:搞懂SynchronizationContext

```C# public async void AsyncVoidMethodThrowsException() {     throw new Exception("Hmmm, something went wrong!"); }

public void ThisWillNotCatchTheException() {     try     {         AsyncVoidMethodThrowsException();     }     catch(Exception ex)     {         //The below line will never be reached         Debug.WriteLine(ex.Message);     } }

public async Task AsyncTaskMethodThrowsException() {     throw new Exception("Hmmm, something went wrong!"); }

public async Task ThisWillCatchTheException() {     try     {         await AsyncTaskMethodThrowsException();     }     catch (Exception ex)     {         //The below line will actually be reached         Debug.WriteLine(ex.Message);     } } ```

Async void方法很難被測試

返回 Task 而不是返回 await

```C# public async Task AsyncTask() {     //Not great!     //...Non-async stuff happens here     //The await is the very last line of the code path - There is no continuation after it     return await GetData(); }

public Task JustTask() {     //Better!     //...Non-async stuff happens here     //Return a Task instead     return GetData(); }

```

因為每次將一個方法宣告為 async 時,編譯器都會建立一個狀態機類來封裝這個方法邏輯,這增加了一定量的開銷。如果這個方法不需要非同步,而是返回一個 Task<T>,讓其他合適的地方處理它, 使方法的返回型別為 Task<T> (而不是 async T),這樣就避免了狀態機的生成,從而使程式碼更簡潔,耗時更少。

但是,凡事都有例外,如果返回一個 Task<T>,那麼返回將立即發生,因此,如果程式碼在 try/catch 塊中,則不會捕獲異常。類似地,如果程式碼在 using塊中,它將立即釋放物件。

```C#

Task DoSomethingAsync() {     using (var foo = new Foo())     {         //會立即返回,那麼foo會被Dispose掉,所以會丟擲異常         return foo.DoAnotherThingAsync();     } }

//可以正常工作

async Task DoSomethingAsync() {     using (var foo = new Foo())     {         return await foo.DoAnotherThingAsync();     } }

```

避免使用 .Wait() 或者 .Result,使用.GetAwaiter().GetResult() 代替

使用.Wait()或者.Result有在 GUI 應用程式中發生死鎖

預設中,當一個未完成的 Taskawait的時候,當前的上下文將會在該Task完成的時候重新獲得並繼續執行剩餘的程式碼。這個context就是當前的SynchronizationContext ,除非它是空的。GUI應用程式的SynchronizationContext有排他性,只允許一個執行緒執行。

```C# public class DeadlockClass {     private static async Task DelayAsync()     {         await Task.Delay(1000);     }

// 當這個方法訪問介面元素時,哈哈,死鎖就出來了     public static void Test()     {         // 開始delay         var delayTask = DelayAsync();         // 等待Delay的結束         delayTask.Wait();     } }

private void TextButton_OnClick(object sender, RoutedEventArgs e) {     DeadlockClass.Test();     TextButton.Content = "Helius"; } ```

在 UI 執行緒上呼叫DeadlockClass.Test(),當 await 完成的時候,它試圖在它原來的程式碼上下文(UI執行緒)執行它剩餘的部分,但是該程式碼上下文已經有一個執行緒在了,就是那個一直在同步等待 async 完成的那個執行緒,它們兩個相互等待,然後就死鎖了。

另外一點需要注意的是,控制檯並不會導致死鎖,控制檯的SynchronizationContext 類似於一個執行緒池的機制而不是排他的。因此當await結束的時候,它可以重新獲得原來的上下文然後執行完剩餘程式碼。這個不同是很多人產生困惑的根源,當他們在控制檯測試的時候程式還OK,用GUI程式跑就發生了死鎖。

使用.Wait() 或者 .Result將會將異常封裝在AggregateException中,這會增加錯誤處理的複雜性。

```C#

class Program {     //MainAsync 中的 try/catch 會捕獲特定異常型別,但是如果將 try/catch 置於 Main 中,則它會始終捕獲 AggregateException。     static void Main(string[] args)     {         Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");         MainAsync().Wait();         Console.ReadKey();     }

static async Task MainAsync()     {         try         {             await Task.Delay(1000);             Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");         }         catch (Exception ex)         {         }             } } ```

高階的.GetAwaiter().GetResult() 將返回一個常規的異常

```C#

public void GetAwaiterGetResultExample() {     //This is ok, but if an error is thrown, it will be encapsulated in an AggregateException     string data = GetData().Result;     //This is better, if an error is thrown, it will be contained in a regular Exception     data = GetData().GetAwaiter().GetResult(); } ```

非同步庫方法應該考慮使用Task.ConfigureAwait(false)來提高效能、避免死鎖

隨著非同步 GUI 應用程式使用,可能會發現async方法的許多小部件都在使用GUI執行緒作為其上下文。這可能會形成遲滯。 ConfigureAwait可以提高效能 。

```C#

public class ConfigureAwaitFalse {     async Task MyMethodAsync()     {         //這裡還是呼叫執行緒的上下文(context)         await Task.Delay(1000);         //這裡還是呼叫執行緒的上下文         await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);         //這裡的上下文就不會是呼叫執行緒的上下文了,而是隨機的上下文         //do Something           } } `` 上面的程式碼與註釋已經非常明顯了。所以有一點是需要注意的,就是如果方法中在await後還需要用到上下文的,就不能設定ConfigureAwait(false),比如GUI程式中,如果方法中await`後還有一些介面元素的操作,就會丟擲執行緒異常了。      

```C#

private async void TextButton_OnClick(object sender, RoutedEventArgs e) {     TextButton.IsEnabled = false;     try     {         await Task.Delay(2000).ConfigureAwait(false);     }     finally     {         TextButton.IsEnabled = true;//這裡會丟擲異常     } } ```

可以修改為: ```C# private async void TextButton_OnClick(object sender, RoutedEventArgs e) {     TextButton.IsEnabled = false;     try     {         //await Task.Delay(2000).ConfigureAwait(false);         await HandleClickAsync();     }     finally     {         TextButton.IsEnabled = true;     } }

private async Task HandleClickAsync() {     await Task.Delay(2000).ConfigureAwait(false); } `` Task 完成後,synchronization context(同步上下文)將呼叫post()方法恢復到原來的位置。 但是,在編寫庫程式碼時,很少需要返回到以前的上下文。當使用task.configureawait(false)` 時,程式碼將不再嘗試恢復到以前的位置。這稍微提高了效能,並有助於避免死鎖。

task.configureawait(true)時,如果可能,程式碼將在完成任務的執行緒中完成,從而避免了上下文切換。

使用 Task.Delay,而不使用Thread.Sleep使任務等待一段時間後執行。

使用Task.WaitAny 等待任意任務完成,使用 Task.WaitAll 等待所有任務完成。