为什么同步访问Task.Result不会死锁?

Why access Task.Result in the synchronous mode doesn't cause a deadlock?

众所周知在UI线程中访问Task的Result属性,同步模式会死锁

理论上遵循代码会死锁但不会。你能解释一下为什么吗?

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

//MVC action
public ActionResult Index()
{
      var result = System.Threading.Tasks.Task.Run(async () => await GetJsonAsync(...)).Result; // deadlock is expectation but not :(

    ...
}

我认为 System.Threading.Tasks.Task.Run(async () => await GetJsonAsync(...)).Result 在某些方面类似于 GetJsonAsync(...).Result,但事实并非如此。

GetJsonAsync(...)).Result会死锁 但是 System.Threading.Tasks.Task.Run(async () => await GetJsonAsync(...)).Result 不是。

Result 本身 不会导致死锁。当从单线程上下文 如果 有一个 await 用于该任务也需要该上下文时,它会导致死锁。

More details:

  • await 默认情况下捕获上下文并在该上下文中恢复。 (您可以使用 ConfigureAwait(false) 覆盖此默认行为并改为在线程池线程上恢复。)
  • Result 阻塞当前线程,直到 Task 完成。 (您可以使用 await 异步使用任务以避免阻塞线程。)
  • 一些上下文是单线程上下文;也就是说,它们一次只允许一个线程进入。例如,ASP.NET Classic 具有单线程请求上下文。 (您可以在线程池线程上使用 Task.Run 到 运行 代码,线程池上下文不是单线程上下文。)

因此,要获得死锁,您需要有一个捕获单线程上下文的 await,然后在该上下文中阻塞线程(例如,对该任务调用 Result ). await 需要上下文来完成 Task,但上下文一次只允许一个线程,而 Result 会在该上下文中保持线程阻塞,直到 Task 完成。

在您的示例中,您在 Task.Run 中调用 GetJsonAsync,它 运行 位于线程池中。因此 GetJsonAsync 中的 await(以及传递给 Task.Run 的委托中的 await)捕获线程池上下文,而不是 ASP.NET 请求线程上下文。然后你的代码调用 Result,它 确实 阻塞了 ASP.NET 请求线程(及其上下文),但是因为 await 不需要那个上下文,没有死锁。