新聞中心
本文轉(zhuǎn)載自微信公眾號「精益碼農(nóng)」,作者精益碼農(nóng)。轉(zhuǎn)載本文請聯(lián)系精益碼農(nóng)公眾號。

引言
C#異步編程語法糖async/await,使開發(fā)者很容易就能編寫異步代碼。
零散看過很多文章,很多是填鴨式灌輸 (有的翻譯文還有偏差)。
遵守以上冷冰冰的②③條的原則,一般可確保異步程序按預期運作,
我們時常能在各大論壇看到同學們(因不遵守②③點)引發(fā)的死鎖現(xiàn)場。
由async/await引起的死鎖現(xiàn)場
UI程序(WinForm、WPF):點擊按鈕,觸發(fā)一個HTTP請求,用請求結(jié)果修改UI控件,以下代碼會引發(fā)deadlock
- public static async Task
GetJsonAsync(Uri uri) - {
- using (var client = new HttpClient())
- {
- var jsonString = await client.GetStringAsync(uri);
- return jsonString;
- }
- }
- // 上層調(diào)用方法
- public void Button1_Click(...)
- {
- var jsonTask = GetJsonAsync(...);
- textBox1.Text = jsonTask.Result;
- }
ASP.NET web程序:從api接口發(fā)起HTTP請求,返回請求的結(jié)果,以下代碼也會引發(fā)deadlock
- public static async Task
GetJsonAsync(Uri uri) - {
- using (var client = new HttpClient())
- {
- var jsonString = await client.GetStringAsync(uri);
- return jsonString;
- }
- }
- // 上層調(diào)用方法
- public class MyController : ApiController
- {
- public string Get()
- {
- var jsonTask = GetJsonAsync(...);
- return jsonTask.Result;
- }
- }
- 解決以上死鎖有兩種編程方式:
- 不再混用異步/同步寫法, 始終使用async/await語法糖編寫異步代碼
- 對等待的異步任務應用ConfigureAwait(false)方法
SynchronizationContext就是這類死鎖的牛鼻子,大多數(shù)時候SynchronizationContext是在異步編程后默默工作,但了解這個對象對于理解sync/await工作原理、解決死鎖大有裨益。
本文會解釋:
- async/await工作機制
- SynchronizationContext在異步編程語法糖中的意義
- 示例代碼為什么會deadlock
1. await/async語法糖工作機制
微軟提出Task線程包裝類、 await/async語法糖簡化了異步編程的方式:
第②步:調(diào)用異步方法GetStringAsync時,開啟異步任務;
第⑥步:遇到await關鍵字,框架會捕獲調(diào)用線程的同步上下文(SynchronizationContext)對象, 附加給異步任務;同時控制權(quán)上交到上層調(diào)用函數(shù);
第⑦步:異步任務完成,通過IO完成端口通知上層線程, 第⑧步:通過捕獲的線程同步上下文執(zhí)行后繼代碼塊;
2. SynchronizationContext的意義
先看下MSDN中關于SynchronizationContext的定義:
提供在各種同步模型中傳播同步上下文的基本功能。此類實現(xiàn)的同步模型的目的是允許公共語言運行庫的內(nèi)部異步/同步操作使用不同的同步模型正常運行。
??這就不是人能看懂的解釋,我給出的解釋是:在線程切換過程中保存調(diào)用線程的上下文環(huán)境, 用于在異步任務完成后使用此線程同步上下文執(zhí)行后繼代碼。
線程同步上下文的意義在哪?
大家都知道:WinForm和WPF都有類似的原則:長耗時的任務在后臺進行,將異步結(jié)果返回給UI線程 。(這難道就是ConfigureAwait方法默認傳true的原因?)
此時就需要捕獲UI線程的SynchronizationContext,并將這個對象傳入異步任務。
- public static void DoWork()
- {
- //On UI thread
- var sc = SynchronizationContext.Current;
- ThreadPool.QueueUserWorkItem(delegate
- {
- //... async task:do work on ThreadPool
- sc.Post(delegate
- {
- // do work on the original context (UI)
- }, null);
- });
- }
SynchronizationContext表示代碼運行的線程環(huán)境,在異步編程中,利用該對象切換代碼執(zhí)行環(huán)境。
不同的.NET框架因各自獨特的線程切換場景有不同的SynchronizationContext子類(重寫父類虛方法):
- ASP.NET有AspNetSynchronizationContext
- WinForm有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. 引言代碼為什么發(fā)生deadlock
觀察引言代碼,控制權(quán)返回到上層調(diào)用函數(shù)時,執(zhí)行流使用Result/(Wait方法)等待任務結(jié)果:Result/Wait()導致調(diào)用線程同步阻塞(等待任務完成), 而異步任務執(zhí)行完成后,會嘗試利用捕獲的同步上下文執(zhí)行后繼代碼,這樣形成死鎖。
正因為如此,我們提出兩種方式解決死鎖:
- 原調(diào)用函數(shù)始終使用await方法,這樣調(diào)用線程是異步等待任務完成,后繼代碼可以在該線程同步上下文上執(zhí)行
- 對異步任務應用ConfigureAwait(false)方法
ConfigureAwait(bool):true 表示嘗試在捕獲的原調(diào)用線程SynchronizationContext 中執(zhí)行后繼代碼;false 不再嘗試在捕獲的線程SynchronizationContext中執(zhí)行后繼代碼。 ConfigureAwait(false) 能解決[因調(diào)用線程同步阻塞]引發(fā)的死鎖,但是同步阻塞沒有利用異步編程的優(yōu)點,不是很推薦。
歸根到底,這兩種解決死鎖的方式都是針對SynchronizationContext;
ASP. NET Core和控制臺程序,因為捕獲的SynchronizationContext=null, 會選擇一個線程同步上下文來執(zhí)行,不會死鎖。
總結(jié)
微軟為加快開發(fā)效率上著實費了心力,.NET提供的await/async語法糖簡化了異步編程方式,
在異步編程中,SynchronizationContext決定了后繼代碼在哪里執(zhí)行的環(huán)境,深入理解這個對象的背景和不同框架的實現(xiàn)方式,能幫助我們避免編寫死鎖代碼。
分享名稱:看過這么多爆文,依舊走不好異步編程這條路?
標題URL:http://m.fisionsoft.com.cn/article/dhsphos.html


咨詢
建站咨詢
