標籤:
平時在使用C# 5.0中的await and async關鍵字的時候總是沒注意,直到今天在調試一個ASP.NET項目時,發現在調用一個聲明為async的方法後,程式老是莫名其妙的被卡住,就算聲明為async的方法中的Task任務執行完畢後,外部方法的await調用還是阻塞著,後來查到了下面這篇文章,才恍然大悟原來await and async模式使用不當很容易造成程式死結,下面這篇文章通過一個Winform樣本和一個Asp.net樣本介紹了await and async模式是如何造成程式死結的,以及如何避免這種死結。
原文連結
UI Example
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.
This code will deadlock.
ASP.NET Example
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):
// 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.
What Causes the Deadlock
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.
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.
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.
So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET):
- The top-level method calls GetJsonAsync (within the UI/ASP.NET context).
- GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context).
- GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.
- GetJsonAsync awaits the Task returned by GetStringAsync. The context is captured and will be used to continue running the GetJsonAsync method later. GetJsonAsync returns an uncompleted Task, indicating that the GetJsonAsync method is not complete.
- The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.
- … Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.
- The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context.
- Deadlock. The top-level method is blocking the context thread, waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for the context to be free so it can complete.
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”.
Preventing the Deadlock
There are two best practices (both covered in my intro post) that avoid this situation:
- In your “library” async methods, use ConfigureAwait(false) wherever possible.
- Don’t block on Tasks; use async all the way down.
這裡我補充一下,如果你開發的是Winform程式,那麼最好用第二種方法避免死結,也就是不要阻塞主線程(也就是本文中提到的context thread),這樣當await等待的Task對象線程執行完畢後,由於主線程沒有被阻塞,因此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.
最後再補充說一點,本文提到的await and async死結問題,在.Net控制台程式中並不存在。因為經過實驗發現在.Net控制台程式中,await後面的代碼預設就是在一個新的線程上執行的,也就是說在控制台程式中就算不調用Task.ConfigureAwait(false),await後面的代碼也會在一個新啟動的線程上執行,不會和主線程發生死結。但是在Winform和Asp.net中就會發生死結。
小心C# 5.0 中的await and async模式造成的死結