美文网首页Swartz动物园
不要阻塞 Async 代码

不要阻塞 Async 代码

作者: 绝望的祖父 | 来源:发表于2017-02-07 23:00 被阅读62次

UI 程序示例

参考下面的代码。一个按钮点击事件会发起一个 REST 调用,并将结果显示在一个 TextBox 中:

public static async Task<JObject> GetJsonAsync(Uri uri)
{
    using (var client = new HttpClient())
    {
        var jsonString = await client.GetStringAsync(uri);
        return JObject.Parse(jsonString);
    }
}

public void Buttion1_Click(...)
{
    var jsonTask = GetJsonAsync(...);
    textBox1.Text = jsonTask.Result;
}

"GetJson" 方法负责发起实际的 REST 请求,并将结果转换为 JSON 对象。而按钮点击处理事件等待 "GetJson" 方法完成,然后显示它的结果。
这段代码将会引起死锁。

ASP.NET 示例

接下来是一个非常简单的示例。我们拥有一个同样的类库方法,它执行一个 REST 请求,只是这一次它被用于 ASP.NET 上下文环境中:

public static async Task<JObject> GetJsonAsync(Uri uri)
{
    using (var client = new HttpClient())
    {
        var jsonString = await client.GetStringAsync(uri);
        return JObject.Parse(jsonString);
    }
}

public class MyController : ApiController
{
    public string Get()
    {
        var jsonTask = GetJsonAsync(...);
        return jsonTask.Result.ToString();
    }
}

基于同样的原因,这段代码也会造成死锁。

是什么造成了死锁

在第一个示例中,当前上下文是 UI 上下文,在第二个示例中,当前上下文则是 ASP.NET 请求上下文(请注意:一个 ASP.NET 请求并不会关联到一个特定的线程,但是同一个请求在同一时刻只会在一个线程中执行)
因此,这就是以上示例代码执行的结果:

  1. 最顶层的方法调用了 GetJsonAsync 方法(在 UI/ASP.NET 上下文中)
  2. GetJsonAsync 方法通过调用 HttpClient.GetStringAsync 发起一个 REST 请求(仍然在同一个上下文中)
  3. GetStringAsync 方法返回一个未完成的 Task,表明 REST 请求还未完成
  4. 接下来 GetJsonAsync 方法将等待 GetStringAsync 返回的任务。此时当前上下文将被捕获,用以在 GetStringAsync 完成后继续执行 GetJsonAsync 方法。GetJsonAsync 返回了一个未完成的任务,指示该方法并未完成
  5. 顶层方法此时将同步阻塞由 GetJsonAsync 方法返回的 Task,这将会阻塞当前线程上下文
  6. 最终,REST 请求将会完成。这会导致由 GetStringAsync 所返回的任务完成
  7. 此时 GetJsonAsync 的后续代码将准备开始执行,此时它将等待当前线程上下文可用,以继续执行下去
  8. 此时死锁产生。顶层方法阻塞了当前上下文,等待 GetJsonAsync 方法完成,而 GetJsonAsync 方法却又在等待当前上下文可用,让它可以完成并继续。

避免死锁

这里有2种方法可以避免死锁:

  • 无论何时,在 async 方法中使用 ConfigureAwait(false)
  • 不要阻塞 Task,无论何时都使用 async

针对第一种方法,新的 GetJsonAsync 方法看起来会像这样:

public static async Task<JObject> GetJsonAsync(Uri uri)
{
    using (var client = new HttpClient())
    {
        var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
        return JObject.Parse(jsonString);
    }
}

这将会修改 GetJsonAsync 方法的等待行为,此时它将不会在当前上下文中恢复执行,而是在线程池中抓取一个线程直接继续执行。这避免了 GetJsonAsync 方法重新进入(re-enter)当前上下文线程

针对第二种方法,此时的顶层方法看起来会像这样:

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();
    }
}

这将修改顶层方法的等待行为,所有的等待都将是异步等待

参考资料

Best practice to call ConfigureAwait for all server-side code
Don't Block on Async Code

相关文章

网友评论

    本文标题:不要阻塞 Async 代码

    本文链接:https://www.haomeiwen.com/subject/pkwmittx.html