这是一个在论坛和 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(); }
这种死锁总是同步和异步代码混合的结果。通常这是因为人们只是在一小段代码中尝试异步,而在其他地方使用同步代码。不幸的是,部分异步代码比使所有内容都异步要复杂和棘手得多。
注意:最好同时应用这两种最佳实践。任何一种都可以防止死锁,但两者都必须应用以实现最大的性能和响应能力。

