为啥总是推荐在每一行上写 ConfigureAwait(false) 并且我真的需要它吗?

Posted

技术标签:

【中文标题】为啥总是推荐在每一行上写 ConfigureAwait(false) 并且我真的需要它吗?【英文标题】:Why is writing ConfigureAwait(false) on every line with await always recommended and do I really need it?为什么总是推荐在每一行上写 ConfigureAwait(false) 并且我真的需要它吗? 【发布时间】:2020-10-11 11:17:51 【问题描述】:

问题不在于 ConfigureAwait 的作用。而是为什么我到处都能看到类似的东西

一般来说,是的。除非方法需要其上下文,否则每次等待都应使用 ConfigureAwait(false)。

即他们建议我应该写

await Method1().ConfigureAwait(false);
await Method2().ConfigureAwait(false);
// Do something else
// ...
await Method3().ConfigureAwait(false);
await Method4().ConfigureAwait(false);

但在这种情况下,只需在开始时重置上下文一次就不会更清楚

await Task.Yield().ConfigureAwait(false);

它保证下面的代码将在没有同步上下文的情况下执行,不是吗?

即我读到如果方法立即返回,编写一次 ConfigureAwait 可能不起作用。 对我来说,显而易见的解决方案看起来像是在肯定不会立即返回的东西上调用 ConfigureAwait(false),Task.Yield 是,对吧?

我也知道 Task.Yield 不再包含 ConfigureAwait(不知道为什么,因为我知道它以前曾经有过),但是查看 Task.Yield 代码很容易编写你的自己的方法,它只会用空的同步上下文调用延续。

对我来说,它似乎更容易阅读,尤其是当你写一次时写起来

await TaskUtility.ResetSyncContext();

而不是在每一行上写 ConfigureAwait。

这会起作用吗(Task.Yield().ConfigureAwait(false) 或类似的自定义方法)还是我错过了什么?

【问题讨论】:

相关:Why would I bother to use Task.ConfigureAwait(continueOnCapturedContext: false); 它曾经是 .NET 世界的规范。请注意,除非您使用 .NET Core 3.1 以 WPF/WinForms 为目标,否则普通 .NET Core 或 ASP.NET Core 应用程序中没有 SynchronizationContext。如果您正在编写 ASP.NET Core 代码,则可以放心地忽略它 @CamiloTerevinto,在 ASP.NET Core 中通常没有同步上下文。 WPF 和 Windows 窗体现在在 .NET Core 上运行并具有同步上下文。没有什么能阻止您向 ASP.NET Core 添加同步上下文。 ConfigureAwait FAQ | .NET Blog 【参考方案1】:

一般来说,是的。除非方法需要其上下文,否则每次等待都应使用 ConfigureAwait(false)。

我经常在 Stack Overflow 上看到这个建议,甚至是 Stephen Cleary(Microsoft MVP)在他的 Async and Await 文章中所说的:

一个好的经验法则是使用 ConfigureAwait(false),除非你知道你确实需要上下文。

Stephen 肯定知道他的东西,我同意该建议在技术上是准确的,但我一直认为这是一个糟糕的建议,原因有两个:

    初学者和 维护风险

首先,这对初学者来说是个坏建议,因为同步上下文是一个复杂的主题。如果您开始学习async/await,被告知“ConfigureAwait(false) 应该用于每个await,除非该方法需要其上下文”,但您甚至不知道“上下文”是什么以及它是什么意思是“需要它”,那么你不知道什么时候不应该使用它,所以你最终总是使用它。这意味着您可能会遇到很难弄清楚的错误,除非您碰巧了解到,是的,您确实需要那个“上下文”东西,而这个神奇的“ConfigureAwait”东西让你失去了它。您可能会浪费数小时试图弄清楚这一点。

对于任何类型的应用程序,我认为建议实际上应该是相反的:根本不要使用ConfigureAwait,除非你知道它的作用并且你确定你绝对不需要在那之后的上下文行。

但是,确定您不需要上下文可能很简单,也可能相当复杂,具体取决于之后调用的方法。但即便如此 - 这是我不同意该建议的第二个原因 - 仅仅因为您不需要该行之后的上下文现在,并不意味着以后不会添加某些代码这将使用上下文。您必须希望进行更改的人知道ConfigureAwait(false) 做了什么,看到并删除了它。在任何地方使用ConfigureAwait(false) 都会产生维护风险。

这是另一位 Stephen Toub(Microsoft 员工)在 ConfigureAwait FAQ 的副标题“我什么时候应该使用 ConfigureAwait(false)?”下的建议:

在编写应用程序时,您通常需要默认行为(这就是为什么它是默认行为)。 ...这导致了以下一般指导:如果您正在编写应用程序级代码,请不要使用ConfigureAwait(false)

在我自己的应用程序代码中,我不会费心去弄清楚我可以在哪里使用它,在哪里不能使用它。我只是忽略了ConfigureAwait 的存在。当然,可以尽可能地使用它来提高性能,但我真的怀疑这对任何人来说都会有明显的差异,即使它可以通过计时器来测量。我不认为投资回报是积极的。

唯一的例外是编写库时,正如 Stephen Toub 在他的文章中指出的那样:

如果您正在编写通用库代码,请使用 ConfigureAwait(false)

这有两个原因:

    库不知道正在使用它的应用程序的上下文,因此它无论如何也无法使用上下文,并且 如果使用该库的人决定同步等待您的异步库代码,则可能会导致他们无法更改的死锁,因为他们无法更改您的代码。 (理想情况下,他们不应该这样做,但它可能会发生)

解决您问题中的另一点:在第一个 await 上使用 ConfigureAwait(false) 并不总是足够的,而不是其余的。在您的库代码中的每个 await 上使用它。 Stephen Toub 的文章标题为“是否可以仅在我的方法中的第一个 await 上使用 ConfigureAwait(false) 而不能在其余部分上使用?”部分说:

如果await task.ConfigureAwait(false) 涉及在等待时已经完成的任务(这实际上非常常见),那么ConfigureAwait(false) 将毫无意义,因为线程在此之后继续执行方法中的代码并且仍然在与以前相同的上下文中。

【讨论】:

同意。我要补充一点,在接受并执行调用者提供的 lambda 的库代码中,在上下文之外调用 lambda 对于调用者来说可能是意外的(例如,它们可能会访问 lambda 内的 UI 元素的属性)。因此,即使在库代码中,使用无条件的 ConfigureAwait(false) 也可能会出现问题。 有趣的是 Polly 库 solves this problem 通过为 API 提供 continueOnCapturedContext 参数,默认为 false 而不是 true。因此,默认情况下,Retry 策略将在第一次调用带有同步上下文的 lambda,而在没有同步上下文的情况下重试。 还要注意 .net-core 标签,它没有同步上下文... @JeremyLakeman ASP.NET Core 不能,但 .NET Core 通常可以。那篇ConfigureAwait FAQ 文章也涵盖了这些内容。 我相信我会更新 async 的博文。在过去的几年中,我还建议仅将 ConfigureAwait(false) 用于库代码。

以上是关于为啥总是推荐在每一行上写 ConfigureAwait(false) 并且我真的需要它吗?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我做的两个div层总是不能在同一行中显示,总是显示成两行?

为啥我的异常堆栈跟踪总是指向最后一个方法行?

mysql 为啥总是推荐使用联合索引而不是单值索引

为啥单选按钮列表总是占据一个新行?

这个C语言小程序在每输入三个数后会不会自动换行,我感觉没问题,为啥运行时不自动换行

为啥latex 中表格一直在每页的开始显示?如何让他出现在我想要的位置?