在.net core中async

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

一、async/await 異步代碼運行流程

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

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

在.net core中async/await阻塞死鎖

二、在異步代碼中阻塞,導致死鎖的示例

UI 示例

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

<code> 

public

 

static

 

async

Task

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; }/<code>

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

這段代碼會死鎖。

ASP.NET 示例

在類庫方法GetJsonAsync中發起一個REST遠程請求,這次這個GetJsonAsync在ASP.NET context中被調用。(示例是Web API項目,同樣適用於任何一個ASP.NET應用程序 - 注:非ASP.NET Core應用)

<code> 

public

 

static

 

async

Task

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();   } }/<code>

這段代碼也會死鎖。與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.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同一時刻只允許被綁定到一個線程上。我曾經在MSDN上有發表過《關於SynchronzationContext》的文章.

死鎖是怎麼發生的呢?我們從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”中的異步方法修改如下:

<code>

public

 

static

 

async

Task

GetJsonAsync

(

Uri uri

)

{   

using

 (

var

 client =

new

 HttpClient())   {     

var

 jsonString =

await

client.GetStringAsync(uri).ConfigureAwait(

false

);     

return

 JObject.Parse(jsonString);   } }/<code>

ConfigureAwait(continueOnCapturedContext: false):continueOnCapturedContext參數表示是否嘗試將延續任務封送回原始上下文。

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

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

<code>

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();   } }/<code>

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

其他”異步等待”指導原則:

執行以下操作…

不要使用以下方式…

使用以下方式

創建Task

Task constructor

Task.Run or TaskFactory.StartNew

檢索後臺任務的結果

Task.Wait 或 Task.Result

await

等待任何任務完成

Task.WaitAny

await Task.WhenAny

檢索多個任務的結果

Task.WaitAll

await Task.WhenAll

等待一段時間

Thread.Sleep

await Task.Delay

五、在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工作的很好,但是也存在棘手的問題,特別是在身份管理方面(參考:Thread.CurrentPrincipal VS HttpContext.Current.User)。

六、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項目


分享到:


相關文章: