Don't Block on Async Code 不要阻塞异步代码
本文是查问题过程中找到的比较好解答,为了记录和便于他人阅读,转载过来(工具翻译过来,翻译的还不错,建议先阅读原文再读译文,有助于提升英文资料能力):
原文地址:https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
这个问题在论坛和Stack Overflow上反复出现。我认为这是异步新手了解到基本知识后提出的最多问题。
UI示例
考虑下面的示例。单击按钮将启动REST调用,并将结果显示在文本框中(此示例适用于Windows窗体,但相同的原理适用于任何 UI应用程序)。
// My "library" method.
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);
return JObject.Parse(jsonString);
}
}
// My "top-level" method.
public void Button1_Click(...)
{
var jsonTask = GetJsonAsync(...);
textBox1.Text = jsonTask.Result;
}
“ GetJson”帮助程序方法负责进行实际的REST调用并将其解析为JSON。按钮单击处理程序等待helper方法完成,然后显示其结果。
此代码将陷入僵局。
ASP.NET示例
这个例子非常相似。我们有一个执行REST调用的库方法,仅这次是在ASP.NET上下文中使用(在这种情况下为Web API,但相同的原理适用于任何 ASP.NET应用程序):
// My "library" method.
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);
return JObject.Parse(jsonString);
}
}
// My "top-level" method.
public class MyController : ApiController
{
public string Get()
{
var jsonTask = GetJsonAsync(...);
return jsonTask.Result.ToString();
}
}
此代码也将陷入僵局。为了同样的原因。
造成僵局的原因
情况就是这样:在我的介绍性帖子中,请记住,当您等待Task后,方法继续执行时,它将在上下文中继续。
在第一种情况下,此上下文是UI上下文(适用于控制台应用程序以外的任何 UI)。在第二种情况下,此上下文是ASP.NET请求上下文。
另外一个很重要的一点:ASP.NET请求上下文不依赖于特定的线程(如UI上下文),但它确实只允许一个线程在同一时间。在AFAIK的任何地方都没有正式记录过这个有趣的方面,但是我在MSDN中有关SynchronizationContext的文章中提到了这一点。
因此,这是从顶级方法(UI的Button1_Click / ASP.NET的MyController.Get)开始发生的事情:
- 顶级方法调用GetJsonAsync(在UI / ASP.NET上下文中)。
- GetJsonAsync通过调用HttpClient.GetStringAsync(仍在上下文中)来启动REST请求。
- GetStringAsync返回未完成的任务,指示REST请求未完成。
- GetJsonAsync等待GetStringAsync返回的任务。上下文被捕获,以后将用于继续运行GetJsonAsync方法。GetJsonAsync返回一个未完成的Task,指示GetJsonAsync方法未完成。
- 顶级方法同步阻止GetJsonAsync返回的Task。这将阻塞上下文线程。
- …最终,REST请求将完成。这就完成了GetStringAsync返回的任务。
- GetJsonAsync的延续现在可以运行了,它等待上下文可用,以便可以在上下文中执行。
- 僵局。顶级方法正在阻止上下文线程,等待GetJsonAsync完成,而GetJsonAsync正在等待上下文空闲以便可以完成。
对于UI示例,“上下文”是UI上下文。对于ASP.NET示例,“上下文”是ASP.NET请求上下文。这种死锁可能是由于“上下文”引起的。
防止死锁
有两种避免这种情况的最佳做法(在我的介绍性文章中都有介绍):
- 在“库”异步方法中,尽可能使用ConfigureAwait(false)。
- 不要阻塞任务;一直使用异步。
考虑第一个最佳实践。新的“库”方法如下所示:
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可以完成返回的任务,而不必重新输入上下文。同时,顶级方法确实需要上下文,因此不能使用ConfigureAwait(false)。
使用ConfigureAwait(false)避免死锁是一种危险的做法。你将不得不使用ConfigureAwait(false)的每一个 await在所有被拦截的代码调用的方法,传递闭包,包括所有的第三方和第二方的代码。使用ConfigureAwait(false)避免死锁充其量只是一种破解)。
正如该帖子的标题所指出的那样,更好的解决方案是“不要阻止异步代码”。
考虑第二个最佳实践。新的“顶级”方法如下所示:
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();
}
}
这将更改顶级方法的阻止行为,从而使上下文永远不会真正被阻止;所有“等待”都是“异步等待”。
注意:最好同时应用两种最佳实践。任一种都可以防止死锁,但是必须同时应用这两者才能获得最佳性能和响应能力。
资源资源
- 我对异步/等待的介绍是一个很好的起点。
- Stephen Toub的博客文章Await,UI和僵局!天啊!涵盖了这种确切类型的死锁(2011年1月,不少!)。
- 如果您喜欢视频,斯蒂芬·图布(Stephen Toub)现场演示了这种僵局(39:40-42:50,但整个演示过程很棒!)。Lucian Wischik还使用VB 演示了此僵局(17: 10-19 :15)。
- 在异步/等待FAQ进入细节上什么时候上下文被捕获并用于延续。
这种死锁始终是将同步代码与异步代码混合在一起的结果。通常这是因为人们只是在尝试使用一小段代码异步,而在其他地方都使用同步代码。不幸的是,部分异步代码比使所有内容异步都复杂和棘手。
如果您确实需要维护部分异步的代码库,请确保另外查看Stephen Toub的两篇博客文章:用于同步方法的异步包装和用于异步方法的同步包装,以及我的AsyncEx库。
浙公网安备 33010602011771号