最近在深入研究异步模式和async, await关键字的时候看到了Stephen Cleary的这篇文章感觉又提高了一下对这两个keyword的了解,原文链接如下
http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
UI Example
考虑下面的示例。单击按钮将启动其他呼叫和显示在文本框中 (此示例是为 Windows 窗体,但同样的原则适用于任何 UI 应用程序) 的结果
// 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; }
"GetJsonAsync"方法进行实际的 REST 调用并解析 JSON。按钮单击方法等待帮助器方法结束,然后显示其结果。此代码将死锁。
ASP.NET Example
和上面非常相似的例子,执行 REST 调用库方法,只是这次它 ASP.NET 上下文中使用 (示例是Web API ,但适用于任何 ASP.NET 应用程序)
// 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(); } }
同样原因,这段代码也会造成死锁。
为什么会死锁?
第一个要点:一个等待另一个任务完成的方法要继续执行时是在一个上下文中执行。在第一种情况下,这种情况下是一个 UI 上下文 (适用于任何 UI 除了控制台应用程序)。在第二种情况下,是 ASP.NET 请求上下文。
另一个要点 ︰ ASP.NET 请求上下文不依赖于特定的线程 (用户界面是),但它一次仅允许一个线程进入。
下面是会发生的事情,我们从顶层方法 (Button1_Click UI / MyController.Get 为 ASP.NET)开始说:
- 顶层的方法调用 GetJsonAsync (在UI/ASP.NET 上下文内)。
- GetJsonAsync 调用 HttpClient.GetStringAsync来进行REST请求
- GetStringAsync 返回未完成的Task,表示请求未完成
- GetJsonAsync 在GetStringAsync 返回的Task上进行await。这时上下文状态被捕获,将用来稍后继续运行 GetJsonAsync 方法。GetJsonAsync 返回未完成的Task,表明 GetJsonAsync 没有结束。
- 顶层方法阻塞 GetJsonAsync 返回的任务。也就是阻塞了上下文线程。
- …过了一会,REST请求完成。GetStringAsync任务完成。
- GetJsonAsync 现在可以继续运行了,它等待它的上下文。
- 死锁。顶级方法阻止上下文的线程,等待 GetJsonAsync 来完成,而 GetJsonAsync 等待上下文被释放。
在UI 示例中,"上下文"是用户界面上下文; 在ASP.NET 示例中,"上下文"是 ASP.NET 请求上下文。
怎样避免死锁
有两个避免这种情况的最佳做法。
- 在异步方法的代码库中,尽可能使用 ConfigureAwait(false)。
- 不要阻塞任务; 从头到尾使用异步。
考虑的第一种。库方法应该这样写:
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); } }
这将更改 GetJsonAsync 的继续执行的方法行为,它将在线程池线程上恢复。从而继续完成任务,而不必等待上下文返回。
第二种方法。"顶层"方法应该这样写:
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(); } }
这将更改顶层方法阻塞行为,上下文从来没有真正阻止;所有的"等待"是"异步等待"。
注意 ︰ 这两种方法任何一个都可以避免死锁,但应该同时使用来获得最快的响应速度和系统性能。