引言:
多線程編程/異步編程非常復雜,有很多概念和工具需要去學習,貼心的.NET提供Task線程包裝類和await/async異步編程語法糖簡化了異步編程方式。
相信很多開發者都看到如下異步編程實踐原則:
實踐原則 | 說明 | 例外情況 | |
① | 避免 Async Void | 最好使用 async Task 方法而不是 async void 方法 | 事件處理程序 |
② | 始終使用 await | 不要混合阻塞式代碼和異步代碼 | 控制台 main 方法 |
③ | 配置上下文 | 盡可能使用ConfigureAwait(false) | 需要上下文的方法 |
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; }
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(); } }

-
不要混合使用異步、同步代碼,始終使用async/await語法糖編寫異步代碼
-
在等待的異步任務內應用ConfigureAwait(false)方法 (:不再嘗試從捕獲的同步上下文執行異步編程的后續代碼)
-
為什么要有SynchronizationContext 對象
-
闡述await關鍵字與SynchronizationContext對象交互原理
-
以上代碼為什么會有deadlock, 另外ASP.NET Core為什么不會發生以上死鎖
1. The Need for 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 的關系?

② 應用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在線程切換后幫我們有能力執行各種騷操作。
感謝您的認真閱讀,如有問題請大膽斧正,如果您覺得本文對你有用,不妨右下角點個或加關注。
本文版權歸作者所有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置注明本文的作者及原文鏈接,否則保留追究法律責任的權利。