如果异步调用不一定在不同的线程上执行,那么阻塞异步调用如何导致死锁?

问题描述

我最近阅读了 Stephen Cleary 的关于在同步方法调用异步代码时可能发生的死锁的文章https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

关于这里稍微修改的示例(我添加的只是写行代码):

// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  Console.WriteLine("Before await");
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri).ConfigureAwait(true);
    return JObject.Parse(jsonString);
  }
}

// My "top-level" method.
public void Button1_Click(...)
{
  var jsonTask = GetJsonAsync(...);
  textBox1.Text = jsonTask.Result;
}

他的解释是顶级方法阻塞了 UI 线程等待 GetJsonAsync 完成,而 GetJsonAsync 正在等待 UI 线程被释放以便它可以完成执行。

我的问题是,GetJsonAsync 不是已经在 UI 线程上了吗?为什么它需要等待它被释放?根据这篇文章 here 调用异步函数不一定会创建另一个线程来执行该方法。那么如果 GetJsonAsync 一直在 UI 线程上执行,它会如何导致 UI 线程出现问题?就像当 Console.WriteLine() 被执行时,如果不是在 UI 线程上,它是在哪里完成的?我觉得我在这里遗漏了一些东西,但不知道是什么。

澄清:执行在什么时候离开 UI 线程/上下文并需要返回?关于需要返回但永远不会离开线程/上下文的讨论太多了。

解决方法

我要问的是,如果从 Button1_Click 调用 GetJsonAsync 时,如果此调用没有创建新线程来执行,它会在哪里执行。在 GetJsonAsync 中的 await 之前,它不是还在 UI 上下文中执行 Console.WriteLine(...) 吗?

我建议阅读我的async intro。总结:

每个异步方法开始同步执行。这段代码:

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

在 UI 线程上调用 GetJsonAsync,它确实开始在 UI 线程上执行。它在 UI 线程上执行 Console.WriteLine,在 UI 线程上 new 启动客户端,甚至在 UI 线程上调用 GetStringAsync。它从该方法返回一个任务,然后await完成它(为了简单起见,我忽略了 ConfigureAwait(true))。

await 是事物可能变得异步的点。任务未完成(即,客户端还没有收到字符串),因此 GetJsonAsync 返回一个未完成的任务给它的调用者。然后 Button1_Click 阻塞 UI 线程,等待该任务完成(通过调用 .Result)。

因此,当前状态为 GetJsonAsync 不再在 UI 线程上运行。是 not actually "running" anywhere

稍后,当那个字符串结果到达时,从GetStringAsync返回的任务完成,GetJsonAsync需要继续执行。它尚未在 UI 线程上;现在不在任何地方。由于 await 捕获了一个 UI 上下文,它将尝试在该上下文(在 UI 线程上)恢复。