为什么同步方式访问Task.Result不会出现死锁?

1 c# deadlock async-await

众所周知,在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 :(

    ...
}
Run Code Online (Sandbox Code Playgroud)

我认为这在某种程度上System.Threading.Tasks.Task.Run(async () => await GetJsonAsync(...)).Result是相似的,但事实并非如此。GetJsonAsync(...).Result

GetJsonAsync(...)).Result会陷入僵局,但System.Threading.Tasks.Task.Run(async () => await GetJsonAsync(...)).Result事实并非如此。

Ste*_*ary 6

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

更多细节

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

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

在您的示例中,您在GetJsonAsynca 内部调用Task.Run,它在线程池上运行它。因此awaitin GetJsonAsync(以及await传递给 的委托中Task.Run)捕获线程池上下文,而不是 ASP.NET 请求线程上下文。然后您的代码调用Result,这确实会阻止 ASP.NET 请求线程(及其上下文),但由于await不需要该上下文,因此不会出现死锁。