Don't Block on Async Code 不要阻止异步代码

翻译自 Don't Block on Async Code (stephencleary.com)

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.
这是论坛和 Stack Overflow 上反复提出的问题。我认为这是异步新手在学习了基础知识后最常问的问题。

UI Example UI 示例

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).
请考虑以下示例。单击按钮将启动 REST 调用并在文本框中显示结果(此示例适用于 Windows 窗体,但相同的原则适用于任何 UI 应用程序)

// My "library" method.
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);
    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.
“GetJson” 帮助程序方法负责进行实际的 REST 调用并将其解析为 JSON。按钮单击处理程序等待帮助程序方法完成,然后显示其结果。

This code will deadlock. 此代码将死锁

ASP.NET Example ASP.NET 示例

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 any ASP.NET application):
这个例子非常相似;我们有一个执行 REST 调用的库方法,只是这次它在 ASP.NET 上下文中使用(在本例中为 Web API,但相同的原则适用于任何 ASP.NET 应用程序)

// My "library" method.
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);
    return JObject.Parse(jsonString);
  }
}

// My "top-level" method.
public class MyController : ApiController
{
  public string Get()
  {
    var jsonTask = GetJsonAsync(...);
    return jsonTask.Result.ToString();
  }
}

 

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.
情况是这样的:请记住,在我 await 一个 Task 之后,当方法继续时,它将在上下文中继续。

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.
在第一种情况下,此上下文是 UI 上下文(适用于除 Console 应用程序之外的任何 UI)。在第二种情况下,此上下文是 ASP.NET 请求上下文。

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.
另一个重要的点是:ASP.NET 请求上下文不绑定到特定线程(就像 UI 上下文一样),但它一次只允许一个线程进入。AFAIK 在任何地方都没有正式记录这个有趣的方面,但在我关于 SynchronizationContext 的 MSDN 文章中提到了它。

So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET):
所以这就是发生的事情,从顶级方法(Button1_Click 用于 UI / MyController.Get 用于 ASP.NET):

  1. The top-level method calls GetJsonAsync (within the UI/ASP.NET context).
    顶级方法调用 GetJsonAsync (在 UI/ASP.NET 上下文中) 。
  2. GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context).
    GetJsonAsync 通过调用 HttpClient.GetStringAsync(仍在上下文中)来启动 REST 请求。
  3. GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.
    GetStringAsync 返回一个未完成的 Task,指示 REST 请求未完成。
  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.
    GetJsonAsync 等待 GetStringAsync 返回的 Task。上下文已捕获,并将用于稍后继续运行 GetJsonAsync 方法。GetJsonAsync 返回一个未完成的 Task,表示 GetJsonAsync 方法未完成。
  5. The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.
    顶级方法同步阻止 GetJsonAsync 返回的 Task。这会阻止上下文线程。
  6. … Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.
    …最终,REST 请求将完成。这将完成 GetStringAsync 返回的 Task。
  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.
    GetJsonAsync 的延续现在已准备好运行,它正在等待上下文可用,以便它可以在上下文中执行。
  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.
    死锁。顶级方法是阻止上下文线程,等待 GetJsonAsync 完成,而 GetJsonAsync 正在等待上下文空闲,以便它可以完成。

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”.
对于 UI 示例,“context” 是 UI 上下文;对于 ASP.NET 示例,“context” 是 ASP.NET 请求上下文。这种类型的死锁可能是由 “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.
    在你的 “library” 异步方法中,尽可能使用 ConfigureAwait(false)。
  2. Don’t block on Tasks; use async all the way down.
    不要阻止 Tasks;一直使用 async。

Consider the first best practice. The new “library” method looks like this:
考虑第一个最佳实践。新的 “library” 方法如下所示:

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

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. The top-level methods, meanwhile, do require the context, so they cannot use ConfigureAwait(false).
这会更改 GetJsonAsync 的延续行为,使其不会在上下文中恢复。相反,GetJsonAsync 将在线程池线程上恢复。这使 GetJsonAsync 能够完成它返回的 Task,而无需重新进入上下文。同时,顶级方法确实需要上下文,因此它们不能使用 ConfigureAwait(false)。

 

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).
使用 ConfigureAwait(false) 来避免死锁是一种危险的做法。对于阻止代码调用的所有方法(包括所有第三方和第二方代码),必须对传递闭包中的每个await 使用 ConfigureAwait(false)。使用 ConfigureAwait(false) 来避免死锁充其量只是一个 hack)。

As the title of this post points out, the better solution is “Don’t block on async code”.
正如本文的标题所指出的,更好的解决方案是 “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”.
这会更改顶级方法的阻塞行为,以便上下文永远不会真正被阻塞;所有 “waits” 都是 “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.
这种死锁始终是 synchronous 代码与 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.
如果您确实需要维护部分异步代码库,请务必查看 Stephen Toub 的另外两篇博客文章:Asynchronous Wrappers for Synchronous Methods 和 Synchronous Wrappers for Asynchronous Methods,以及我的 AsyncEx 库

Answered Questions 已回答的问题

There are scores of answered questions out there that are all caused by the same deadlock problem. It has shown up on WinRT, WPF, Windows Forms, Windows Phone, MonoDroid, Monogame, and ASP.NET.
有大量已回答的问题都是由相同的死锁问题引起的。它已出现在 WinRT、WPF、Windows Forms、Windows Phone、MonoDroid、Monogame 和 ASP.NET 上。

posted @ 2024-09-26 17:25  Josen_Earth  阅读(11)  评论(0编辑  收藏  举报