歡迎光臨
每天分享高質量文章

Async/Await中阻塞死鎖

來自:滴答的雨

連結:http://www.cnblogs.com/heyuquan/p/10242361.html

主要是講解在async/await中使用阻塞式程式碼導致死鎖的問題,以及如何避免出現這種死鎖。內容主要是從作者Stephen Cleary的兩篇博文中翻譯過來.


原文1:Don’tBlock on Async Code


原文2:why the AspNetSynchronizationContext was removed


示例程式碼:async_await中阻塞死鎖.rar

一、async/await 非同步程式碼執行流程

async/await是在.NET4.5 版本引入的關鍵字,讓開發者可以更加輕鬆的建立非同步方法。

我們從下圖來認識async/await的執行流程:

二、在非同步程式碼中阻塞,導致死鎖的示例

UI 示例


單擊一個按鈕,將發起一個REST遠端請求並且將結果顯示到textbox控制元件上。(這是一個Windows Forms程式,同樣也適用於其他任何UI應用程式)

// My "library" method.
public static async Task GetJsonAsync(Uri uri)
{
 using (var client = new HttpClient())
 {
   var jsonString = await client.GetStringAsync(uri);
   return JObject.Parse(jsonString);
 }
}

// My "top-level" method.
public void Button1_Click(...)
{
 var jsonTask = GetJsonAsync(...);
 textBox1.Text = jsonTask.Result;
}

類庫方法GetJsonAsync發起REST遠端請求並且將結果解析為JSON傳回。Button1_Click方法呼叫Task .Result阻塞等待GetJsonAsync處理完畢並顯示結果

這段程式碼會死鎖。

ASP.NET 示例

在類庫方法GetJsonAsync中發起一個REST遠端請求,這次這個GetJsonAsync在ASP.NET context中被呼叫。


(示例是Web API專案,同樣適用於任何一個ASP.NET應用程式 – 註:非ASP.NET Core應用)

// My "library" method.
public static async Task 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();
 }
}

這段程式碼也會死鎖。與UI示例是同一個原因。

在《傳統asp.net小心 async/await坑》這篇文章中抓到一個異常資訊,我自己沒註意抓異常,死鎖後就關閉除錯了。不關閉除錯,死鎖一段時間應該會報下麵錯誤。

System.Web.ThreadContext.AssociateWithCurrentThread(Boolean setImpersonationContext)
System.Web.HttpApplication.OnThreadEnterPrivate(Boolean setImpersonationContext)
System.Web.LegacyAspNetSynchronizationContext.CallCallbackPossiblyUnderLock(SendOrPostCallback callback, Object state)
System.Web.LegacyAspNetSynchronizationContext.CallCallback(SendOrPostCallback callback, Object state)
System.Web.LegacyAspNetSynchronizationContext.Post(SendOrPostCallback callback, Object state)
System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation.PostAction(Object state)
System.Threading.Tasks.AwaitTaskContinuation.RunCallback(ContextCallback callback, Object state, Task& currentTask)

--- 引發異常的上一位置中堆疊跟蹤的末尾 ---
System.Threading.Tasks.AwaitTaskContinuation.<>c.<ThrowAsyncIfNecessary>b__18_0(Object s)
System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
System.Threading.ThreadPoolWorkQueue.Dispatch()
System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()

三、是什麼原因導致的死鎖呢?

await一個Task後,在恢復繼續執行時,會試圖進入await之前的context。


第一個示例中,這個context是UI context(任何UI應用,除了控制檯應用)。


第二個示例中,這個context是ASP.NET request context。


另一個需要註意的點:ASP.NET request context 沒有系結到特定的執行緒上(像UI context一樣),但是request context同一時刻只允許被系結到一個執行緒上。

死鎖是怎麼發生的呢?我們從top-level方法開始(UI的Button1_Click方法或ASP.NET的MyContoller.Get方法)

1、top-level方法呼叫GetJsonAsync(在UI/ASP.NET context中)。

2、GetJsonAsync透過HttpClient.GetStringAsync發起REST遠端請求(在UI/ASP.NET context中)。

3、GetStringAsync傳回一個未完成Task,標識REST遠端請求還未處理完

4、GetJsonAsync方法中await GetStringAsync傳回的未完成Task。等Task執行完畢,會重新捕獲等待之前的context並使用它繼續執行GetJsonAsync。

5、GetJsonAsync中await後,攜帶context的執行緒會跳出GetJsonAsync方法,繼續執行後面的程式碼。併在jsonTask.Result發生阻塞。此時攜帶context的執行緒被阻塞了。

6、最終,REST請求處理完,GetStringAsync傳回的Task處理完成。

7、GetJsonAsync方法準備繼續執行,並且等待context可用,以便在context中執行。

8、發生context死鎖。在top-level方法中已經阻塞了攜帶context的執行緒,等待GetJsonAsync傳回的Task完成。而此時,GetJsonAsync方法正在等待context被釋放,以便在context中繼續執行。

四、防止死鎖

這裡有三個最佳實踐來避免這種死鎖(更詳細的傳送門)。

1、在你的”library”非同步方法中,傳回未完成Task時都呼叫ConfigureAwait(false)。

2、始終使用 Async,不要混合阻塞式程式碼和非同步程式碼。

3、ASP.NET 升級為ASP.NET Core。在ASP.NET Core框架中,已經移除SynchronizationContext

按照第一條最佳實踐,”library”中的非同步方法修改如下:

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

ConfigureAwait(continueOnCapturedContext: false):continueOnCapturedContext引數表示是否嘗試將延續任務封送回原始背景關係。

ConfigureAwait(false)改變了GetJsonAsync的延續行為,使它不用在原來的context中恢復。GetJsonAsync將直接在執行緒池執行緒中恢復,這使得GetJsonAsync能完成任務,並且無需重新進入原來的context

 

按照第二條最佳實踐。修改”top-level”方法如下:

public async void Button1_Click(...)
{
 var json = await GetJsonAsync(...);
 textBox1.Text = json;
}

public class MyController : ApiController
{
 public async Task<string> Get()
 
{
   var json = await GetJsonAsync(...);
   return json.ToString();
 }
}

這樣修改,改變了top-level方法的阻塞行為。所有的”等待”都是”非同步等待”,這樣context就不會被阻塞。

其他”非同步等待”指導原則:

五、在ASP.NET Core框架中,已經移除SynchronizationContext

為什麼AspNetSynchronizationContext在ASP.NET Core中被移除。


儘管我不知道ASP.NET團隊內部是什麼觀點,但我認為的觀點是兩個:效能和簡單。

效能方面:

在沒有SynchronizationContext的ASP.NET Core中,當一個async/await非同步處理恢復執行時,會從執行緒池中獲取一個執行緒並且執行繼續操作。

1、避免了把操作排隊到request context佇列(request context同一時刻只允許被系結到一個執行緒上)

2、避免了因為攜帶 request context 的執行緒被阻塞而發生“死鎖”

3、不需要重新進入request context(重新進入request context涉及到很多內部作業任務,例如:設定HttpContext.Current和當前執行緒的身份標識(identity)和語言(culture))

簡單化:

舊版本ASP.NET中SynchronizationContext工作的很好,但是也存在棘手的問題,特別是在身份管理方面。

六、async/await避免阻塞式死鎖最佳實踐

1、在你的“library”非同步方法中,傳回未完成Task時都呼叫ConfigureAwait(false),標識不需要將延續任務封送回原始背景關係。

儘管在ASP.NET Core中,不用再呼叫ConfigureAwait(false)來防止死鎖。但由於你提供的“類庫”可能被用於UI 應用(eg:winform、wpf等)、舊版本ASP.NET應用、其他還存在context的應用。

2、更好的解決方案是“始終使用 async,不要混合阻塞式程式碼和非同步程式碼”。

因為當你在非同步程式碼中阻塞程式,將失去非同步程式碼帶來的所有好處。並且非同步程式碼釋放當前執行緒帶來的伸縮性也會失效。

3、將ASP.NET專案升級為ASP.NET Core專案



編號249,輸入編號直達本文

●輸入m獲取文章目錄

推薦↓↓↓

資料庫開發

更多推薦25個技術類微信公眾號

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。