这是一个在论坛和 Stack Overflow 上反复提出的问题。我认为这是异步新手在学习了基础知识后最常问的问题。
用户界面示例
勇哥编写了下面的例子。单击按钮将启动 REST 调用并在文本框中显示结果(此示例适用于 Windows 窗体,但相同的原则适用于任何UI 应用程序)。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
var jsonTask = GetJsonAsync(new Uri("http://www.baidu.com"));
textBox1.Text = jsonTask.GetAwaiter().GetResult().ToString();
}
public static async Task<int> GetJsonAsync(Uri uri)
{
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri);
return 0;
}
}
}
}
代码的功能是按下按钮,调用异步方法GetJsonAsync从网站取到文本,显示到textbox中去。
这里勇哥在异步方法中直接返回了一个0,没有用到获取到的文本内容。
这段代码运行后会死锁。
导致死锁的原因
情况是这样的:从我的介绍文章中记住,在等待 Task 之后,当方法继续时,它将在 context 中继续。
在第一种情况下,此上下文是一个 UI 上下文(适用于除控制台应用程序之外的任何UI)。在第二种情况下,此上下文是 ASP.NET 请求上下文。
另外一个很重要的一点:ASP.NET请求上下文不依赖于特定的线程(如UI上下文),但它确实只允许一个线程在同一时间。这个有趣的方面在 AFAIK 的任何地方都没有正式记录,但在我关于 SynchronizationContext 的 MSDN 文章中提到。
所以这就是发生的事情,从顶级方法(用于 UI 的 Button1_Click / 用于 ASP.NET 的 MyController.Get)开始:
1. 顶级方法调用GetJsonAsync(在ui的上下文中)
2. GetJsonAsync通过调用client.GetStringAsync(仍在ui上下文中)启动rest请求
3. client.GetStringAsync返回一个未完成的task,表示rest请求未完成。
4. GetJsonAsync等待client.GetStringAsync返回的Task。上下文被捕获,
稍后将用于继续运行GetJsonAsync方法。
GetJsonAsync返回一个未完成的Task,说明GetJsonAsync方法未完成。
5. 顶层方法同步阻塞GetJsonAsync返回的Task。这会阻塞上下文线程。
6. 最终,rest请求完成。这完成了由client.GetStringAsync返回的任务。
7. GetJsonAsync的延续现在已准备好运行,它等待上下文可用,以便它可以在上下文中执行。
8. 僵局。顶层方法正在阻塞上下文线程,等待GetJsonAsync完成,
而GetJsonAsync正在等待上下文空闲以便它可以完成。
对于 UI 示例,“上下文”是 UI 上下文;对于 ASP.NET 示例,“上下文”是 ASP.NET 请求上下文。任何一种“上下文”都可能导致这种类型的死锁。
防止死锁
在您的“库”异步方法中,尽可能使用 ConfigureAwait(false)。
不要阻塞任务;一直使用异步。
考虑第一个最佳实践。新的“GetJsonAsync”方法如下所示:
注意仅仅是添加了方法.ConfigureAwait(false);
public static async Task<int> GetJsonAsync(Uri uri)
{
using (var client = new HttpClient())
{
var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
return 0;
}
}.ConfigureAwait(false) 改变GetJsonAsync的延续行为,因此,它并没有上下文恢复。相反,GetJsonAsync 将在线程池线程上恢复。这使 GetJsonAsync 能够完成它返回的任务,而无需重新输入上下文。同时,顶级方法确实需要上下文,因此它们不能使用ConfigureAwait(false).
使用ConfigureAwait(false)来避免死锁是一种危险的做法。 你将不得不使用ConfigureAwait(false)的每一个 await在所有被拦截的代码调用的方法,传递闭包,包括所有的第三方和第二方的代码。 使用ConfigureAwait(false)以避免死锁充其量只是一个黑客)。 正如这篇文章的标题所指出的,更好的解决方案是“不要阻塞异步代码”。
第二种防死锁的办法是一路异步,如下:
这改变了顶级方法的阻塞行为,因此上下文永远不会被真正阻塞;所有“等待”都是“异步等待”。
private async void button1_Click(object sender, EventArgs e)
{
//var jsonTask = GetJsonAsync(new Uri("http://www.baidu.com"));
//textBox1.Text = jsonTask.GetAwaiter().GetResult().ToString();
var jsonTask = await GetJsonAsync(new Uri("http://www.baidu.com"));
textBox1.Text = jsonTask.ToString();
}这种死锁总是同步和异步代码混合的结果。通常这是因为人们只是在一小段代码中尝试异步,而在其他地方使用同步代码。不幸的是,部分异步代码比使所有内容都异步要复杂和棘手得多。
注意:最好同时应用这两种最佳实践。任何一种都可以防止死锁,但两者都必须应用以实现最大的性能和响应能力。


少有人走的路



















