抓住異步編程 async-await 語法糖的牛鼻子
引言
C# 異步編程語法糖 async/await,使開發者很容易就能編寫異步代碼。零散看過很多文章,很多是填鴨式灌輸 (有的翻譯文還有偏差)。
遵守以上冷冰冰的②③條的原則,可以確保我們的異步程序按照預期運作,但是我們常看到違背這 2 條原則引發的死鎖現場。
由 async/await 引起的死鎖現場
UI 例子:
點擊按鈕觸發一個 HTTP 請求,用請求的返回值修改 UI 控件, 以下代碼會引發 deadlock (類似狀態出現在 WinForm、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 web 例子:
從 api 發起遠程 HTTP 請求,等待請求的結果,以下代碼也會引發 deadlock
public static async Task<JObject> GetJsonAsync(Uri uri)
{
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri);
return JObject.Parse(jsonString);
}
}
// 上層調用方法
public class MyController : ApiController
{
public string Get()
{
var jsonTask = GetJsonAsync(...);
return jsonTask.Result.ToString();
}
}
解決以上死鎖有 2 種編程寫法:
-
不再混用異步、同步寫法, 始終使用 async/await 語法糖編寫異步代碼
-
在等待的異步任務時應用 ConfigureAwait(false) 方法
SynchronizationContext
就是解決這類死鎖的牛鼻子,大多數時候SynchronizationContext
是在異步編程後默默工作的,但是瞭解這個對象對於理解 Task、await/sync 工作原理大有裨益。
本文會解釋:
-
async/await 工作機制
-
SynchronizationContext 在異步編程語法糖中的意義
-
爲什麼會有 deadlock
1、await/async 語法糖工作機制
微軟提出了 Task 線程包裝類和 await/async 簡化了異步編程的方式:
第②步:調用異步方法 GetStringAsync 時,開啓異步任務;
第⑥步:遇到 await 關鍵字,框架會捕獲調用線程的同步上下文 (SynchronizationContext) 對象, 附加給異步任務;同時,控制權上交到上層調用函數;
第⑦步:異步任務完成,通過 IO 完成端口通知上層線程, 第⑧步:通過捕獲的線程同步上下文執行後繼代碼塊;
2、SynchronizationContext 的意義
先看下 MSDN 中關於 SynchronizationContext 的定義:
提供在各種同步模型中傳播同步上下文的基本功能。此類實現的同步模型的目的是允許公共語言運行庫的內部異步 / 同步操作使用不同的同步模型正常運行。
☹️這完全不是人能看懂的解釋,我給出的解釋是:在線程切換過程中保存調用線程的上下文, 用於在異步任務完成後使用此線程同步上下文執行後繼代碼。
這個線程同步上下文的意義在哪?
我們大家都知道:WinForm 和 WPF 都有類似的原則:長耗時的任務在後臺計算,將異步結果返回給 UI 線程
這個時候我們就需要捕獲 UI 線程的 SynchronizationContext,並將這個對象傳入後臺線程。
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 子類(通常是重寫父類虛方法):
-
ASP.NET 有 AspNetSynchronizationContext
-
Windows Form 有 WindowsFormSynchronizationContext
-
WPF 有 DispatcherSynchronizationContext
-
ASP.NET Core、控制檯程序不存在 SynchronizationContext,SynchronizationContext.Current=null
AspNetSynchronizationContext 維護了 HttpContext.Current、用戶身份和文化,但在 ASP. NET Core 這些信息天然依賴注入,故不再需要 SynchronizationContext;另一個好處是不再獲取同步上下文對性能也是一種提升。因此,對於 ASP.NET Core 程序,ConfigureAwait(false) 不是必需的,然而,在基礎庫時最好還是使用 ConfigureAwait(false),因爲你保不準上層會混用同步 / 異步代碼。
3、引言代碼爲什麼發生 deadlock
觀察引言代碼,控制權返回到上層調用函數時,執行流使用 Result/(Wait 方法) 等待任務結果,Result/Wait() 會導致調用線程同步阻塞 (等待任務完成), 而異步任務執行完成後,會嘗試利用捕獲的同步上下文執行後繼代碼,這樣形成死鎖。
正因爲如此,我們提出:
-
在調用函數始終使用 await 方法,這樣調用線程是異步等待任務完成,後繼代碼可以在該線程同步上下文上執行
-
對異步任務應用 ConfigureAwait(false) 方法
ConfigureAwait(bool):true 表示嘗試在捕獲的原調用線程 SynchronizationContext 中執行後繼代碼;false 不再嘗試在捕獲的線程 SynchronizationContext 中執行後繼代碼。ConfigureAwait(false) 能解決 [因調用線程同步阻塞] 引發的死鎖,但是同步阻塞沒有利用異步編程的優點,不是很推薦。
你會看到,這兩種緩解死鎖的方案其實 都是針對SynchronizationContext
;ASP.NET Core 和控制檯程序,因爲捕獲的 SynchronizationContext=null, 會選擇一個線程同步上下文來執行,不會死鎖。
總結
微軟爲加快開發效率上着實費了心力,.NET 提供的 await/async 語法糖簡化了異步編程方式, 在異步編程中,SynchronizationContext 決定了後繼代碼在哪裏執行的環境,深入理解這個對象的背景和不同框架的實現方式,能幫助我們避免編寫死鎖代碼。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/fIBDXyZwxPZZ1CaS_QESKA