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


引言:

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

相信很多開發者都看到如下異步編程實踐原則:

  實踐原則  說明  例外情況
 ①  避免 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 對象

  • 闡述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

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

本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置注明本文的作者及原文鏈接,否則保留追究法律責任的權利。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM