Async方法死锁的问题 Don't Block on Async Code(转)

今天调试requet.GetRequestStreamAsync异步方法出现不返回的问题,可能是死锁了。看到老外一篇文章解释了异步方法死锁的问题,懒的翻译,直接搬过来了。

http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

This is a problem that is brought up repeatedly on the forums and Stack Overflow. I think it’s the most-asked question by async newcomers once they’ve learned the basics.

UI Example

Consider the example below. A button click will initiate a REST call and display the results in a text box (this sample is for Windows Forms, but the same principles apply to any UI application).

// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  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;
}

The “GetJson” helper method takes care of making the actual REST call and parsing it as JSON. The button click handler waits for the helper method to complete and then displays its results.

This code will deadlock.

ASP.NET Example

This example is very similar; we have a library method that performs a REST call, only this time it’s used in an ASP.NET context (Web API in this case, but the same principles apply to anyASP.NET application):

// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  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();
  }
}

This code will also deadlock. For the same reason.

What Causes the Deadlock

Here’s the situation: remember from my intro post that after you await a Task, when the method continues it will continue in a context.

In the first case, this context is a UI context (which applies to any UI except Console applications). In the second case, this context is an ASP.NET request context.

One other important point: an ASP.NET request context is not tied to a specific thread (like the UI context is), but it does only allow one thread in at a time. This interesting aspect is not officially documented anywhere AFAIK, but it is mentioned in my MSDN article about SynchronizationContext.

So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET):

  1. The top-level method calls GetJsonAsync (within the UI/ASP.NET context).
  2. GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context).
  3. GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.
  4. GetJsonAsync awaits the Task returned by GetStringAsync. The context is captured and will be used to continue running the GetJsonAsync method later. GetJsonAsync returns an uncompleted Task, indicating that the GetJsonAsync method is not complete.
  5. The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.
  6. … Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.
  7. The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context.
  8. Deadlock. The top-level method is blocking the context thread, waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for the context to be free so it can complete.

For the UI example, the “context” is the UI context; for the ASP.NET example, the “context” is the ASP.NET request context. This type of deadlock can be caused for either “context”.

Preventing the Deadlock

There are two best practices (both covered in my intro post) that avoid this situation:

  1. In your “library” async methods, use ConfigureAwait(false) wherever possible.
  2. Don’t block on Tasks; use async all the way down.

Consider the first best practice. The new “library” method looks like this:

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

This changes the continuation behavior of GetJsonAsync so that it does not resume on the context. Instead, GetJsonAsync will resume on a thread pool thread. This enables GetJsonAsync to complete the Task it returned without having to re-enter the context.

Using ConfigureAwait(false) to avoid deadlocks is a dangerous practice. You would have to use ConfigureAwait(false) for every await in the transitive closure of all methods called by the blocking code, including all third- and second-party code. Using ConfigureAwait(false) to avoid deadlock is at best just a hack).

As the title of this post points out, the better solution is “Don’t block on async code”.

Consider the second best practice. The new “top-level” methods look like this:

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

This changes the blocking behavior of the top-level methods so that the context is never actually blocked; all “waits” are “asynchronous waits”.

Note: It is best to apply both best practices. Either one will prevent the deadlock, but both must be applied to achieve maximum performance and responsiveness.

Resources

This kind of deadlock is always the result of mixing synchronous with asynchronous code. Usually this is because people are just trying out async with one small piece of code and use synchronous code everywhere else. Unfortunately, partially-asynchronous code is much more complex and tricky than just making everything asynchronous.

If you do need to maintain a partially-asynchronous code base, then be sure to check out two more of Stephen Toub’s blog posts: Asynchronous Wrappers for Synchronous Methods and Synchronous Wrappers for Asynchronous Methods, as well as my AsyncEx library.

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏张善友的专栏

Line Counter - Writing a Visual Studio 2005 Add-In

Download original source files - 553 Kb Download new source files - 1483 Kb Do...

2068
来自专栏Java学习123

Install MATE or XFCE on CentOS 7

4197
来自专栏Golang语言社区

Go调试简单的内存泄漏

Memory leaks are a class of bugs where memory is not released even after it is n...

4573
来自专栏运维

linux文件树

以前有意找这方面的资料,今天突然发现在系统中就有 linux系统用man hier solaris用man  filesystem 其结果如下     ...

1022
来自专栏码匠的流水账

聊聊springcloud的GatewayControllerEndpoint

本文主要研究一下springcloud的GatewayControllerEndpoint

3461
来自专栏everhad

转载:Package by feature, not layer

The first question in building an application is "How do I divide it up into pac...

1270
来自专栏技术小黑屋

Issues About Installing Octopress

Actually I am fresh to Write Blog with Octopress in Github Pages.According to th...

1112
来自专栏程序员的酒和故事

2017年12月编程语言排行榜

The programming languages Kotlin and C seem to be the only candidates to become ...

3535
来自专栏大数据学习笔记

Hadoop基础教程-第10章 HBase:Hadoop数据库(10.7 HBase 批量导入)

第10章 HBase:Hadoop数据库 10.7 HBase 批量导入 10.7.1 批量导入数据的方法 向HBase表中导入一条数据可以使用HBase Sh...

2235
来自专栏c#开发者

Processing Binary Documents Through BizTalk Via Web Services[转]

Published 21 July 06 08:24 AM | rseroter Just finishing up a two-week BizTalk P...

3544

扫码关注云+社区

领取腾讯云代金券