Don‘t Block on Async Code(不要阻塞异步代码)

原文地址: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_ClickMyController.Get)开始,程序执行过程如下:

  1. 顶层方法调用GetJsonAsync(在UI上下文中)。
  2. GetJsonAsync通过调用HttpClient.GetStringAsync(仍然在上下文中)来启动REST请求。
  3. GetStringAsync返回一个未完成的Task,表示REST请求尚未完成。
  4. GetJsonAsync等待GetStringAsync返回的Task。该上下文将被捕获,稍后将用于继续运行GetJsonAsync方法。GetJsonAsync返回一个未完成的Task,表明GetJsonAsync方法没有完成。
  5. 顶层方法同步阻塞由GetJsonAsync返回的Task。这会阻塞上下文线程。
  6. 最终,REST请求将完成。这样就完成了GetStringAsync返回的Task。
  7. GetJsonAsync的延续逻辑现在可以运行了,它等待上下文可用,这样它就可以在上下文中执行了。
  8. 死锁。顶层方法阻塞上下文线程,等待GetJsonAsync完成,而GetJsonAsync正在等待上下文空闲以便完成。

防止死锁

有两个最佳实践(都在我的介绍文章中提到)可以避免这种情况:

  1. 在你的“库”异步方法中,尽可能使用ConfigureAwait(false)
  2. 不要阻碍任务;一直使用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();
      }
}

这改变了顶级方法的阻塞行为,因此上下文实际上永远不会被阻塞;所有“等待”都是“异步等待”。

注意:最好同时应用这两种最佳实践。任何一种方法都可以防止死锁,但必须同时应用这两种方法才能实现最大的性能和响应性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值