原文地址:Don’t Block on Async Code
翻译有部分删减,不影响文章核心观点
这是一个在论坛和StackOverflow上反复出现的问题。我认为这是async
新手在学习基础知识后最常问的问题。
UI的例子
考虑下面的例子。单击按钮将启动一个REST调用,并在文本框中显示结果(本示例是针对 Windows Forms 的,但相同的原则适用于任何UI应用程序)。
// “库”方法
public static async Task<JObject> GetJsonAsync(Uri uri)
{
// (实际的代码不应该在using块中使用HttpClient;这只是示例代码)
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;
}
“GetJson”帮助方法负责进行实际的REST调用并将其解析为JSON。按钮单击处理程序等待helper方法完成,然后显示其结果。
此代码将死锁。
导致死锁的原因
这里的情况是:记住我的介绍文章,当你等待一个任务后,当方法继续时,它将在一个上下文中继续。
在第一种情况下,这个上下文是一个UI上下文(它适用于除控制台应用程序之外的任何UI)。在第二种情况下,该上下文是一个ASP.NET请求上下文。
另一要点是:ASP.NET请求上下文不绑定到特定的线程(像UI上下文一样),但它一次只允许一个线程进入。这个有趣的方面,在任何地方都没有正式的文档记录,但是在我关于SynchronizationContext的MSDN文章中提到过。
从顶层方法(UI程序的 Button1_Click
的 MyController.Get
)开始,程序执行过程如下:
- 顶层方法调用
GetJsonAsync
(在UI上下文中)。 GetJsonAsync
通过调用HttpClient.GetStringAsync
(仍然在上下文中)来启动REST请求。GetStringAsync
返回一个未完成的Task,表示REST请求尚未完成。GetJsonAsync
等待GetStringAsync
返回的Task。该上下文将被捕获,稍后将用于继续运行GetJsonAsync方法。GetJsonAsync
返回一个未完成的Task
,表明GetJsonAsync
方法没有完成。- 顶层方法同步阻塞由
GetJsonAsync
返回的Task。这会阻塞上下文线程。 - 最终,REST请求将完成。这样就完成了
GetStringAsync
返回的Task。 GetJsonAsync
的延续逻辑现在可以运行了,它等待上下文可用,这样它就可以在上下文中执行了。- 死锁。顶层方法阻塞上下文线程,等待
GetJsonAsync
完成,而GetJsonAsync
正在等待上下文空闲以便完成。
防止死锁
有两个最佳实践(都在我的介绍文章中提到)可以避免这种情况:
- 在你的“库”异步方法中,尽可能使用
ConfigureAwait(false)
。 - 不要阻碍任务;一直使用
async
。
考虑第一个最佳实践。新的"库"方法是这样的:
public static async Task<JObject> GetJsonAsync(Uri uri)
{
// (real-world code shouldn't use HttpClient in a using block; this is just example code)
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
return JObject.Parse(jsonString);
}
}
这将改变GetJsonAsync
的延续行为,使其不会在上下文上恢复。相反,GetJsonAsync
将在线程池线程上恢复。这使得GetJsonAsync
无需重新进入上下文即可完成它返回的Task
。与此同时,顶层方法确实需要上下文,因此它们不能使用ConfigureAwait(false)
。
使用
ConfigureAwait(false)
来避免死锁是一种危险的做法。对于阻塞代码(包括所有第三方和第二方代码)调用的所有方法的传递闭包中的每个await
,您必须使用ConfigureAwait(false)
。使用ConfigureAwait(false)
来避免死锁充其量只是一个hack
)。
考虑第二个最佳实践。新的“顶级”方法是这样的:
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();
}
}
这改变了顶级方法的阻塞行为,因此上下文实际上永远不会被阻塞;所有“等待”都是“异步等待”。
注意:最好同时应用这两种最佳实践。任何一种方法都可以防止死锁,但必须同时应用这两种方法才能实现最大的性能和响应性。