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

Posted

技术标签:

【中文标题】如果异步调用不一定在不同的线程上执行,阻塞异步调用如何导致死锁?【英文标题】:How can blocking async calls cause a deadlock if async calls aren't necessarily executed on a different thread? 【发布时间】:2021-02-05 01:21:03 【问题描述】:

我最近在此处阅读了 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 线程/上下文并需要返回?有很多关于需要返回的讨论,但从来没有离开线程/上下文。

【问题讨论】:

Stephen 的解释是针对原始代码,而不是针对您修改后的示例 您添加了ConfigureAwait(false),这实际上是修复的一半,而不是导致阻塞的完整代码。您正在将原始作者的结论应用于修改后的代码,同时给人以他编写的印象。两者都没有帮助。用一个新的例子可能会更好。 原文章很好的解释了这个问题。不知道如何改进这里 @MickyD 我不确定您看到的是什么,但在修改后的示例中它清楚地表示ConfigureAwait(true),这在他编写 await 调用时暗示了这一点。我只是想更具体一点。 @avhhh,GetJsonAsync 在 UI 线程上执行,直到第一个 await,然后将 GetJsonAsync 的上下文保存到状态机并添加到消息循环中。消息循环将验证“每个循环”上的所有状态机,完成后会将其上下文加载到 UI 线程并继续在 UI 线程上执行。 【参考方案1】:

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

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

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

public void Button1_Click(...)

  var jsonTask = GetJsonAsync(...);
  textBox1.Text = jsonTask.Result;

在 UI 线程上调用 GetJsonAsync,它确实开始在 UI 线程上执行。它在 UI 线程上执行 Console.WriteLinenews 在 UI 线程上执行客户端,甚至在 UI 线程上调用 GetStringAsync。它从该方法中获取任务,然后awaits 它(为简单起见,我忽略了ConfigureAwait(true))。

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

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

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

【讨论】:

这里特别解释:“所以,当前状态是 GetJsonAsync 不再在 UI 线程上运行。它实际上并没有在任何地方“运行”。是我正在寻找的确切答案。谢谢!

以上是关于如果异步调用不一定在不同的线程上执行,阻塞异步调用如何导致死锁?的主要内容,如果未能解决你的问题,请参考以下文章

C# 同步调用 异步调用 异步回调 多线程的作用

同步异步阻塞非阻塞 总结

阻塞非阻塞同步异步

同步与异步,阻塞与非阻塞

异步/同步,阻塞/非阻塞,单线程/多线程概念梳理

调用fastdfsapi是同步的还是异步的