平時在使用C# 5.0中的await and async關鍵字的時候老是沒注意,直到今天在調試一個ASP.NET項目時,發如今調用一個聲明爲async的方法後,程序總是莫名其妙的被卡住,就算聲明爲async的方法中的Task任務執行完畢後,外部方法的await調用仍是阻塞着,後來查到了下面這篇文章,才恍然大悟原來await and async模式使用不當很容易形成程序死鎖,下面這篇文章經過一個Winform示例和一個Asp.net示例介紹了await and async模式是如何形成程序死鎖的,以及如何避免這種死鎖。html
原文連接json
Consider the example below. A button click will initiate a REST call and display the results in a text box (this sample is for Windows Forms, but the same principles apply to any UI application).安全
// My "library" method. 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 void Button1_Click(...) { var jsonTask = GetJsonAsync(...); textBox1.Text = jsonTask.Result; }
The 「GetJson」 helper method takes care of making the actual REST call and parsing it as JSON. The button click handler waits for the helper method to complete and then displays its results.app
This code will deadlock.異步
This example is very similar; we have a library method that performs a REST call, only this time it’s used in an ASP.NET context (Web API in this case, but the same principles apply to any ASP.NET application):async
// My "library" method. 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(); } }
This code will also deadlock. For the same reason.ide
Here’s the situation: remember from my intro post that after you await a Task, when the method continues it will continue in a context.post
In the first case, this context is a UI context (which applies to any UI except Console applications). In the second case, this context is an ASP.NET request context.ui
One other important point: an ASP.NET request context is not tied to a specific thread (like the UI context is), but it does only allow one thread in at a time. This interesting aspect is not officially documented anywhere AFAIK, but it is mentioned in my MSDN article about SynchronizationContext.this
So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET):
For the UI example, the 「context」 is the UI context; for the ASP.NET example, the 「context」 is the ASP.NET request context. This type of deadlock can be caused for either 「context」.
上面內容的大體意思就是說在使用await and async模式時,await關鍵字這一行後面的代碼塊會被一個context(也就是上面提到的ASP.NET request context和UI context)線程繼續執行,若是咱們將本例中調用top-level method的線程稱爲線程A(即context線程),因爲GetJsonAsync方法也是由線程A調用的,因此當GetJsonAsync方法中await的GetStringAsync方法執行完畢後,GetJsonAsync須要從新使用線程A執行await代碼行以後的代碼,而如今因爲線程A在top-level method的代碼中由於訪問了jsonTask.Result被阻塞了(由於線程A調用top-level method代碼中jsonTask.Result的時候,await的GetStringAsync的Task還沒執行完畢,因此線程A被阻塞),因此GetJsonAsync沒法從新使用線程A執行await代碼行以後的代碼塊,也被阻塞,因此造成了死鎖。也就是說top-level method代碼中線程A由於等待GetJsonAsync中await的GetStringAsync結束被阻塞,而GetStringAsync也等待線程A在top-level method的阻塞結束得到線程A來執行GetJsonAsync中await代碼行後面的代碼也被阻塞,兩個阻塞相互等待,相互死鎖。
There are three best practices (both covered in my intro post) that avoid this situation:
這裏我補充一下,若是你開發的是Winform程序,那麼最好用第二種方法避免死鎖,也就是不要阻塞主線程,這樣當await等待的Task對象線程執行完畢後,因爲主線程沒有被阻塞,所以await後面的代碼就會在恰當的時候(這裏提到的「恰當的時候」是由.Net Framework本身判斷的,.Net Framework會安排主線程在某個時候繼續執行await後面的代碼)繼續在主線程上執行完畢。之因此在Winform中不推薦用第一種方法是由於第一種方法會讓await後面的代碼在另外的線程上執行,而再也不是在主線程上執行,若是await後有代碼設置了Winform控件的值,那麼會引發Winform程序的線程安全問題,因此在Winform中最好的辦法仍是不要阻塞主線程,讓await後面的代碼可以在主線程上執行。但在Asp.net中用上面第一種或第二種方法均可以,不存在線程安全問題。
Consider the first best practice. The new 「library」 method looks like this:
public static async Task<JObject> GetJsonAsync(Uri uri) { using (var client = new HttpClient()) { var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false); return JObject.Parse(jsonString); } }
This changes the continuation behavior of GetJsonAsync so that it does not resume on the context. Instead, GetJsonAsync will resume on a thread pool thread. This enables GetJsonAsync to complete the Task it returned without having to re-enter the context.
Consider the second best practice. The new 「top-level」 methods look like this:
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(); } }
This changes the blocking behavior of the top-level methods so that the context is never actually blocked; all 「waits」 are 「asynchronous waits」.
Note: It is best to apply both best practices. Either one will prevent the deadlock, but both must be applied to achieve maximum performance and responsiveness.
The third best practice:若是想結束async & await模式的調用,啓動一個新的線程去await異步方法的返回結果:
// My "library" method. 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 string Get() { string jsonResultString = string.Empty; Task.Run(async () => { jsonResultString = await GetJsonAsync(...); //await以後的代碼 }).Wait();//此處啓動線程是爲了防止Async & Await模式形成死鎖 return jsonResultString; }
這樣由於GetJsonAsync方法是由Task.Run新啓動的線程來調用的,因此在await GetJsonAsync(...)執行完畢以後,.Net Framework就會用Task.Run新啓動的線程來執行await以後的代碼(而Task.Run啓動的新線程執行到await GetJsonAsync(...)後就進入閒置狀態了,因此一旦await的GetJsonAsync方法真正執行完畢後,因爲Task.Run啓動的新線程如今沒有被阻塞,因此就能夠當即被用來執行await以後的代碼),不會和top-level method的線程(即context線程)相互阻塞,形成死鎖。
最後再補充說一點,本文提到的await and async死鎖問題,在.Net控制檯程序中並不存在。由於通過實驗發如今.Net控制檯程序中,await關鍵字這一行後面的代碼默認就是在一個新的線程上執行的,也就是說在控制檯程序中就算不調用Task.ConfigureAwait(false),await關鍵字這一行後面的代碼也會在一個新啓動的線程上執行,不會和主線程發生死鎖。可是在Winform和Asp.net中就會發生死鎖。