異步編程最佳實踐

避免async void

異步方法返回類型有3種,void,Task和Task<T>,void儘可能不要使用。html

原理剖析:dom

使用async void標記的方法有不一樣的錯誤處理語義。async Task或async Task<T>方法拋出異常時,異常會被捕獲並放到Task對象上。然而,標記爲async void的方法沒有Task對象,因此async void方法拋出的任何異常都會直接放到SynchronizationContext(異步上下文)上,它是在async void方法開始的時候激活的。下面是一個例子:異步

//async void 方法不能被捕獲的異常
private async void ThrowExceptionAsync()
{
  throw new InvalidOperationException();
}
public void AsyncVoidExceptions_CannotBeCaughtByCatch()
{
  try
  {
    ThrowExceptionAsync();
  }
  catch (Exception )
  {
    //異常不會被捕獲
    throw;
  }
}

async void有不一樣的組成語法。返回Task或Task<T>的async方法可使用await Task.WhenAny或Task.WhenAll等輕易組合。而返回void的async方法沒有提供簡單的方式來通知它們已經完成的調用代碼。啓用若干個async void方法很容易,但不容易決定它們何時完成。async void方法開始和完成時會通知它們的SynchronizationContext,可是自定義的SynchronizationContext對於常規應用代碼是一個複雜的解決方案。 async

async void方法測試很困難。因爲錯誤處理和組合的差別,編寫調用async void方法的單元測試很困難。 性能

很明顯,async void方法與async Task方法相比有不少劣勢,但在一個特殊場合頗有用,那就是異步的事件句柄。它們直接將異常拋出到SynchronizationContext,這與同步的事件句柄表現很類似。同步的事件句柄一般是私有的,所以它們不能被組合或者直接測試。我想採起的方法是在異步事件句柄中最小化代碼,好比,讓它await一個包含實際邏輯的async Task方法,代碼以下:單元測試

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是一個餿主意。spa

下面是一個簡單的例子:一個方法阻塞了異步方法的結果。在控制檯程序中會工做的很好,可是從GUI或者ASP.Net上下文中調用的時候就會死鎖。死鎖的實際緣由是當調用Task.Wait的時候進一步開啓了調用棧。線程

//阻塞異步代碼時的一個常見死鎖問題
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方法的完成。它們每個都在等待另外一個,形成了死鎖。 code

注意控制檯程序不會形成這種死鎖。它們有個線程池SynchronizationContext而沒有一次執行一大坨代碼的SynchronizationContext,所以當await完成時,它在線程池線程上調度該async方法的剩餘部分。該方法能夠完成,它完成了返回task,並無死鎖。

總之,應該避免混合async和阻塞的代碼。這樣作的話會形成死鎖,更復雜的錯誤處理和上下文線程不可預測的阻塞。

配置上下文

能夠查看個人另外一篇博客《Async and Await 異步和等待》的「避免上下文」部分。

這裏稍加補充以下:

除了性能方面以外,ConfigureAwait還有另外一個重要的方面:它能夠避免死鎖。在「一直使用async」的代碼示例中,再次思考一下:若是你在DelayAsync代碼行添加「ConfigureAwait(false)」,那麼死鎖就會避免。此次,當await完成時,它嘗試在線程池上下文內執行async方法的剩餘部分。該方法能夠完成,完成後返回task,而且沒有死鎖。這項技術對於逐漸將應用從同步轉爲異步特別有用。

建議將ConfigureAwait用在方法中的每一個await以後。只有當未完成的Task被等待時,纔會喚起上下文被捕獲;若是Task已經完成了,那麼上下文不會被捕獲。

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.

}

 

每一個異步方法都有本身的上下文,所以若是一個異步方法調用另外一個異步方法,那麼它們的上下文是獨立的。

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;
  }
}
今天就寫到這裏吧,還有不少很高級的用法,須要本身好好研究一下才能分享出來,但願你們多多支持!
相關文章
相關標籤/搜索