【異步編程】Part2:掌控SynchronizationContext避免deadlock

引言:

  多線程編程/異步編程很是複雜,有不少概念和工具須要去學習,貼心的.NET提供Task線程包裝類await/async異步編程語法糖簡化了異步編程方式。編程

相信不少開發者都看到以下異步編程實踐原則:json

  實踐原則  說明  例外狀況
 ①  避免 Async Void  最好使用 async Task 方法而不是 async void 方法  事件處理程序
 ②  始終使用 await  不要混合阻塞式代碼和異步代碼  控制檯 main 方法
 ③  配置上下文  儘量使用ConfigureAwait(false)  須要上下文的方法
  
  遵照以上冷冰冰的②③條的原則,可保證異步程序按照預期狀態正常運做;咱們在各大編程論壇常看到違背這2條原則引起的莫民奇妙的死鎖問題。

  UI 例子:點擊按鈕觸發了一個遠程HTTP請求,用請求的返回值修改UI控件, 如下代碼會引起deadlock (相似狀態出如今Windows Form、WPF)多線程

public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

// 頂層調用方法
public void Button1_Click(...)
{
  var jsonTask = GetJsonAsync(...);
  textBox1.Text = jsonTask.Result;
}
  ASP.NET例子: API Action發起遠程HTTP請求,等待請求的json結果,並解析json字符串,如下代碼也會引起deadlock
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}
// My "top-level" method.
public class MyController : ApiController
{
  public string Get()
  {
    var jsonTask = GetJsonAsync(...);
    return jsonTask.Result.ToString();
  }
}
 
    解決以上deadlock需利用以上第②③條編程原則:
  • 不要混合使用異步、同步代碼,始終使用async/await語法糖編寫異步代碼框架

  • 在等待的異步任務內應用ConfigureAwait(false)方法 (:再也不嘗試從捕獲的同步上下文執行異步編程的後續代碼)異步

   第②③條原則與咱們今天的主角SynchronizationContext 密切相關,大多數時候SynchronizationContext 是在異步編程後面默默工做的, 可是瞭解這個對象對於理解Task、await/sync 工做原理大有裨益。本文會解釋
  • 爲何要有SynchronizationContext 對象async

  • 闡述await關鍵字與SynchronizationContext對象交互原理異步編程

  • 以上代碼爲何會有deadlock, 另外ASP.NET Core爲何不會發生以上死鎖函數

 

1. The Need for SynchronizationContext

  先看下MSDN中關於SynchronizationContext的定義:
提供在各類同步模型中傳播同步上下文的基本功能。此類實現的同步模型的目的是容許公共語言運行庫的內部異步/同步操做使用不一樣的同步模型正常運行。

  上面的定義給個人印象是:在線程切換過程當中保存前置線程執行的上下文環境。工具

  咱們你們都知道:Windows Form和WPF都基於相似的原則: 不容許在非UI線程上操做 UI元素學習

    

  這個時候咱們能夠捕獲當前執行環境SynchronizationContext,利用這個對象切換回原UI線程。

public static void DoWork()
{
    //On UI thread
    var sc = SynchronizationContext.Current;

    ThreadPool.QueueUserWorkItem(delegate
    {
       // do work on ThreadPool
        sc.Post(delegate
        {
             // do work on the original context (UI)
        }, null);
    });
}

      SynchronizationContext表示代碼正在運行的當前環境,每一個線程都有本身的SynchronizationContext,經過SynchronizationContext.Current可獲取當前線程的同步上下文。在異步線程切換場景中,咱們並不須要代碼在哪一個線程上啓動,只須要使用SynchronizationContext ,就能夠返回到啓動線程。

  不一樣的.NET框架因各自獨特的需求有不一樣SynchronizationContext子類(一般是重寫Post虛方法):

  - 默認SynchronizationContext封裝的是線程池內線程,將執行委託發送到線程池中任意線程。

  - asp.Net有AspNetSynchronizationContext,在一個異步page處理過程當中,context始終使用的是線程池中某個特定線程

  - Windows Form有WindowsFormSynchronizationContext,封裝單個UI線程,Post方法將委託傳遞給 Control.BeginInvoke

  - WPF 有DispatcherSynchronizationContext, 瞭解到與WinForm 相似。

 

2. await/async語法糖與SynchronizationContext 的關係?

  以上ThreadPool.QueueUserWorkItem 涉及線程底層,微軟提出Task線程包裝類和 await/async 簡化了異步編程的方式:
 
  
  ① 調用異步方法GetStringAsync時,.NET框架爲咱們建立了異步任務T;

  ② 應用await時,框架捕獲當前環境, 存儲在SynchronizationContext 對象並附加於以上Task;

  ③ 同時,控制權返回到原上層調用函數,返回一個未完成的Task<int>對象,這個時候須要關注上層調用函數使用 await異步等待仍是使用Result/Wait()方式同步等待

  ④ 異步任務T執行完成,await以後的代碼將會成爲continuation block, 默認狀況下利用捕獲的SynchronizationContext 對象執行該continuation block 代碼。

    內部實際是將continuation block代碼放入SynchronizationContext 的Post方法。

    

3.引言代碼爲何發生deadlock, 而ASP.NET Core/控制檯程序爲何不會發生相似deadlock?

        仔細觀察引言代碼,控制返回到 上層調用函數時, 該調用函數使用Result屬性去等待任務結果,Result/Wait()等同步方式會致使調用線程掛起等待任務完成。而在異步方法內部,await觸發的異步任務執行完成後,會嘗試利用捕獲的同步上下文執行剩餘代碼,而該同步上下文中的線程正同步等待整個異步任務完成,造成死鎖。

正由於如此,咱們提出:

  - 在原調用函數始終 使用 await方法,這樣該線程是異步等待 任務完成。

  - 在異步任務內部應用ConfigureAwait(false)方法, 不嘗試使用捕獲的同步上下文執行後繼代碼

MSDN ConfigureAwait(): true to attempt to marshal the continuation back to the original context captured; otherwise, false

  另外注意:ASP.NET Core,,控制檯程序不存在SynchronizationContext , 故不會發生相似的死鎖。

 

 總結:

  雖然await/async 語法糖讓咱們在編寫.NET 異步程序時駕輕就熟、爲所欲爲,可是不要忘記了SynchronizationContext 在其中轉承起合的做用。

利用可以保存當前執行代碼的上下文特性,SynchronizationContext在線程切換後幫咱們有能力執行各類騷操做。

 
做者: JulianHuang

感謝您的認真閱讀,若有問題請大膽斧正,若是您以爲本文對你有用,不妨右下角點個或加關注。

本文版權歸做者全部,歡迎轉載,但未經做者贊成必須保留此段聲明,且在文章頁面明顯位置註明本文的做者及原文連接,不然保留追究法律責任的權利。

相關文章
相關標籤/搜索