【问题标题】:How ConfigureAwait(false) Prevent Ui DeadlocksConfigureAwait(false) 如何防止 UI 死锁
【发布时间】:2021-01-09 00:45:56
【问题描述】:

我知道这个问题已经被无休止地问过了,但仍然存在。我想我错过了什么。 当我们想要以异步方式更新 UI(为了参数而使用 WinForm UI)时。但是我们不能使用 async /await 关键字,我们必须使用 ConfigureAwait(false)。据我了解,它“告诉”它可以在任何可用线程上恢复自身而不是等待主线程的任务。它通过释放 UI 来防止死锁,因为它不等待长进程完成并且任务不等待主 UI 线程可用。

以下代码演示了经典的死锁

public void Button_Click()
{

    SomeTextBox.Text =LongProcessAsync().Result;
}

所以现在我的问题开始了:)。长时间处理任务完成后,UI 线程最终如何更新 UI。

是因为任务将结果传递给另一个 UI 进程来完成工作吗? UI 队列消息如何与该部分相关? 当说只有一个线程更新 ui 时,这是否意味着该线程在应用程序的整个生命周期中都存在,或者只有一个线程正在更新 ui 但可以创建一个新线程并执行这些操作?

谢谢

【问题讨论】:

  • 但是我们不能使用 async /await 关键字,我们必须使用 ConfigureAwait(false):谁告诉你的?任务的目的是什么?作品完成后更新 UI?然后,您需要以某种方式在 UI 线程中恢复。你想控制更新的时间,这样就不会在异步操作完成后立即发生吗?运行一个任务并使用IProgress<T> 委托来接收更新,一次或每次任务有新数据时,最终可以缓存。否则,使用 async/await 模式。很大程度上取决于操作的类型。

标签: asp.net multithreading winforms task task-parallel-library


【解决方案1】:

我建议阅读 Stephen Cleary 关于此主题的博文:Don't Block on Async Code,其中解释了死锁是如何发生的以及如何避免它。

注意“死锁”和“UI阻塞”的区别。当两个线程相互等待时会发生死锁,当 UI 线程忙/阻塞并且无法处理 UI 消息时会发生阻塞 UI。这里使用ConfigureAwait(false) 并不能防止“阻塞的 UI”,而是防止“死锁”。

当您编写运行任务的异步库方法时,为防止可能出现的死锁,建议通过ConfigureAwait(false) 运行您的任务。这是为了防止死锁,即使您的库的用户通过调用ResultWait 获得您的异步方法的结果。

ConfigureAwait(false) 基本上是告诉:在这一行之后不要回到原来的上下文,继续在线程池线程上执行。

为了更好地理解它,看这个例子:

// UI code 
1:  private void button1_Click(object sender, EventArgs e) 
2:  { 
3:      var result = GetDataAsync().Result; 
4:      MessageBox.Show(result); 
5:  } 

// A library code
6:  public async Task<string> GetDataAsync() 
7:  { 
8:      await Task.Delay(1000); 
9:      return "Data"; 
10: } 

考虑以下事实:

  • 在 Windows 窗体中,所有 UI 代码都在单个线程中执行; UI 线程。

  • TaskResult 方法会阻塞调用线程,直到任务的结果准备好。

这就是这里发生的事情:

  • 在第 3 行,GetDataAsync 将在 UI 线程中被调用并执行,直到第 8 行。
  • 在第 8 行,将创建一个任务并将在线程池线程上运行,await 告诉在运行该任务后,应在先前捕获的上下文(即 UI 线程上下文)中继续执行。
  • 未完成的任务返回给原始调用者(在第 3 行),该任务将在未来完成(在完成类似 8 的任务然后运行第 9 行之后)。
  • 然后将执行第 3 行的 Result 方法,这会阻塞任务,直到它完成并且 GetDataAsync 的最终结果准备好。
  • 当第 8 行的等待任务完成时,调度程序尝试在 UI 线程中运行第 9 行,但 UI 线程被阻塞!所以第 9 行无法执行,GetDataAsync 也无法完成。

发生死锁:UI 线程正在等待 GetDataAsync 完成,而 GetDataAsync 正在等待主线程空闲来执行其余代码。

为了避免死锁,你应该使用await,而不是通过WaitResult获取异步方法的结果。但正如我之前提到的,在上述场景中,如果您作为库开发人员通过ConfigureAwait(false) 运行您的任务(此处为Task.Delay),它可以防止死锁,因为它告诉不要在此行之后返回原始上下文并继续执行在线程池线程上。所以在第 8 行,它告诉在线程池线程中继续,所以当 UI 线程被阻塞时,但第 9 行执行并将数据返回给 UI 线程并解除阻塞。

// UI code 
1:  private void button1_Click(object sender, EventArgs e) 
2:  { 
3:      var result = GetDataAsync().Result; 
4:      MessageBox.Show(result); 
5:  } 

// A library code
6:  public async Task<string> GetDataAsync() 
7:  { 
8:      await Task.Delay(1000).ConfigureAwait(false); 
9:      return "Data"; 
10: } 

但同样,请记住,作为 UI 开发人员,您应该始终使用await 来等待Task,并且您不应该使用ResultWait

【讨论】:

  • 感谢您的学术回答,对我帮助很大,非常感谢!!。正如您所提到的, ConfigureAwait(false) 可以防止死锁,因为它告诉不要回到原始上下文 (UI) 。但最终,结果还是返回到了阻塞的 UI 线程。所以如果它被阻塞了,它如何将结果显示到 UI 上?
  • 为了更清楚一点,使用 ConfigureAwait(false) 的第二个场景:UI 线程在第 3 行被阻塞,等待在踏池线程上运行的GetData,它进入到@ 987654348@,在第 8 行等待 1 秒,然后不返回 UI 线程,在第 9 行返回结果,然后执行控制返回 UI 线程,并解除阻塞。不会发生死锁,但您的 UI 在执行 GetData 期间不再响应。现在清楚了吗?
  • 注意“死锁”和“阻塞的UI”的区别。当两个线程相互等待时会发生死锁,当 UI 线程忙或阻塞并且无法处理 UI 消息时会发生阻塞 UI。这里使用ConfigureAwait(false) 并不能防止“阻塞的 UI”,而是防止“死锁”。
  • 我想我明白了,你的意思是返回的执行(第9行)不是来自UI线程,它来自线程池然后返回到UI(主执行线程)下一次执行?
  • 我稍微改写了答案,您可以从“考虑以下事实”开始重新阅读。除了我在答案顶部链接的 Stephen Cleary 的文章之外,您可能还想阅读this article。但我希望我重新表述的解释能帮助您更好地了解情况:)
【解决方案2】:

我们不能使用 async /await 关键字

为什么不呢?做同步工作会阻塞UI线程,会降低用户体验。使用async/await 几乎总是更好。

它“告诉”任务

从技术上讲,它告诉await。这是一个常见的错误。方法是ConfigureAwait,而不是ConfigureTask

它可以在任何可用线程上恢复自己,而不是等待到主线程。

从技术上讲,它只是避免了上下文that is normally captured by await。对于 UI 线程,此上下文通常会在 UI 线程上恢复。 ConfigureAwait(false) 导致 await 避免该上下文,并像没有上下文一样恢复,即在线程池线程上。

它通过释放 UI 来防止死锁,因为它不等待长进程完成并且任务不等待主 UI 线程可用。

没有。 deadlock 的发生是因为 await 正在等待 UI 线程空闲并且 UI 线程在 async 方法上被阻塞以完成。 ConfigureAwait(false) 通过允许 await 在线程池线程而不是 UI 线程上恢复来避免死锁。 UI 线程仍然被 async 方法阻塞。 UI 线程没有腾出去做其他工作,用户体验仍然下降。

在长进程任务完成后,UI 线程最终如何更新 UI。

在该代码示例中,UI 线程阻塞异步代码,因此在该代码运行时它会阻塞(变得无响应)。代码完成后,UI 线程将继续执行,就像任何其他代码一样。在该方法结束时,它返回到其消息队列 proc,并且应用程序再次响应。

虽然从技术上讲可以有多个 UI 线程,但很少有应用程序具有多个 UI 线程。每个 UI 组件都属于一个特定的 UI 线程;它们不能共享。

【讨论】:

  • 对不起,如果ConfigureAwait(false) 在线程池中的另一个线程上恢复执行时,如果 UI 线程发生在另一个线程上,如何“通知”完整性?我假设await 之后的其余代码应该在 UI 线程上执行,否则 UI 控件的更新将不起作用。
  • @Basin:当 UI 线程阻塞一个任务时,它就像任何其他同步原语一样;它等待任务完成时设置的信号。当 UI 线程 awaits 一个任务时,它(最终)返回到它的消息循环,当该任务完成时,它向消息循环发送一条消息以继续执行 async 方法。
  • 感谢重播!!你说“返回到它的消息循环”是什么意思。可以请我参考这个话题。我想我不太清楚你,但也许这就是我想念的
  • @user14346079:每个 UI 线程都有一个消息循环,当顶级 async 方法对未完成的任务执行 await 时,它会返回到消息循环。
  • 因为大多数时候您不会从头开始编写代码或不使用无法更改以使其异步的其他库。它可能是旧代码或未正确编写的代码。
猜你喜欢
  • 1970-01-01
  • 2016-01-18
  • 2016-02-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多