在 SomethingAsync() 上调用 ConfigureAwait(false) 将是无效的,因为传递给 Task.Run 的委托将在线程池线程上执行,堆栈上没有更高的用户代码,因此 SynchronizationContext.Current 将返回空值。此外,Task.Run 还隐式地使用了 TaskScheduler.Default,这意味着在委托中查询 TaskScheduler.Current 也将返回 Default。这意味着无论是否使用了 ConfigureAwait(false),await 都将表现出相同的行为。此外,它也不保证该 lambda 内部的代码会做什么。如下代码:
Task.Run(async delegate
SynchronizationContext.SetSynchronizationContext(new SomeCoolSyncCtx());
await SomethingAsync(); // will target SomeCoolSyncCtx
SomethingAsync 中的代码实际上就会将 SynchronizationContext.Current 视为 SomeCoolSyncCtx 实例,并且该等待和 SomethingAsync 中任何未配置的等待都会返回到该实例。因此,要使用这种方法,你需要了解你正在排队的所有代码可能会做什么,也可能不会做什么,它的操作是否会妨碍你的操作。
这种方法的代价是需要创建/队列一个额外的任务对象。这对您的应用程序或库来说可能重要,也可能不重要,这取决于您对性能的敏感度。
此外,请记住,这些技巧可能会带来更多问题,并产生其他意想不到的后果。例如,有人编写了静态分析工具(如 Roslyn 分析器)来标记未使用 ConfigureAwait(false) 的等待,如 CA2007。如果你启用了这样的分析器,但又为了避免使用 ConfigureAwait 而使用了这样的技巧,那么分析器很有可能会标记它,从而给你带来更多的工作。因此,也许你会因为分析器的嘈杂而禁用它,而现在你最终会遗漏代码库中其他本应使用 ConfigureAwait(false) 的地方。
我能否使用 SynchronizationContext.SetSynchronizationContext 来避免使用 ConfigureAwait(false)?
不,也许吧。这取决于所涉及的代码。
有些开发人员是这样编写代码的:
Task t;
SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
t = CallCodeThatUsesAwaitAsync(); // awaits in here won't see the original context
finally { SynchronizationContext.SetSynchronizationContext(old); }
await t; // will still target the original context
希望它能让 CallCodeThatUsesAwaitAsync 中的代码将当前上下文视为空。确实如此。因此,如果这段代码运行在某个自定义的 TaskScheduler 上,CallCodeThatUsesAwaitAsync 中的等待(且未使用 ConfigureAwait(false))仍将看到并队列回该自定义 TaskScheduler。
所有注意事项与之前的 Task.Run 相关常见问题解答中的一样:这种变通方法会产生影响,而且 try 内的代码也可以通过设置不同的上下文(或调用非默认 TaskScheduler 的代码)来挫败这些尝试。
对于这种模式,您还需要注意一个细微的变化:
SynchronizationContext old = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);
await t;
finally { SynchronizationContext.SetSynchronizationContext(old); }
看到问题所在了吗?这有点难看,但也有可能造成很大影响。我们无法保证 await 最终会在原始线程上调用回调/继续,这意味着将 SynchronizationContext 重置回原始线程可能不会真正发生在原始线程上,这可能会导致该线程上的后续工作项看到错误的上下文(为了解决这个问题,编写良好的应用程序模型在设置自定义上下文时通常会添加代码,以便在调用任何进一步的用户代码前手动重置上下文)。即使它碰巧运行在同一线程上,也可能要过一段时间才能运行,因此上下文在一段时间内不会得到适当恢复。如果运行在不同的线程上,最终可能会将错误的上下文设置到该线程上。诸如此类,不一而足。这与理想状态相去甚远。
我正在使用 GetAwaiter().GetResult(),我需要使用 ConfigureAwait(false) 吗?
ConfigureAwait 只影响回调。具体来说,awaiter 模式要求 awaiter 公开 IsCompleted 属性、GetResult 方法和 OnCompleted 方法(可选择 UnsafeOnCompleted 方法)。ConfigureAwait 只影响 {Unsafe}OnCompleted 的行为,因此如果你只是直接调用 awaiter 的 GetResult() 方法,那么无论是在 TaskAwaiter 还是在 ConfiguredTaskAwaitable.ConfiguredTaskAwaiter 上进行调用,行为上都不会有任何区别。因此,如果您在代码中看到 task.ConfigureAwait(false).GetAwaiter().GetResult(),您可以将其替换为 task.GetAwaiter().GetResult()(同时也要考虑您是否真的想这样阻塞)。
我知道我运行的环境永远不会有自定义同步上下文或自定义任务调度程序,我可以不使用 ConfigureAwait(false)吗?
也许吧,这取决于你对 “从不 ”这部分有多大把握。正如之前的常见问题中提到的,您正在使用的应用程序模型没有设置自定义同步上下文,也没有在自定义任务调度程序上调用您的代码,但这并不意味着其他用户或库代码不会这样做。因此,您需要确保情况并非如此,或者至少认识到可能存在的风险。
我听说在 .NET Core 中不再需要 ConfigureAwait(false),是真的吗?
错。在 .NET Core 上运行时需要它,原因与在 .NET Framework 上运行时完全相同。这方面没有任何变化。
不过,变化的是某些环境是否发布了自己的 SynchronizationContext。特别是,.NET Framework 上的经典 ASP.NET 有自己的 SynchronizationContext,而 ASP.NET Core 则没有。这意味着在 ASP.NET Core 应用程序中运行的代码默认不会看到自定义的 SynchronizationContext,从而减少了在这种环境中运行 ConfigureAwait(false) 的需要。
但这并不意味着永远不会出现自定义 SynchronizationContext 或 TaskScheduler。如果某些用户代码(或您的应用程序使用的其他库代码)设置了自定义上下文并调用了您的代码,或者在调度到自定义 TaskScheduler 的任务中调用了您的代码,那么即使在 ASP.NET Core 中,您的等待也可能会看到非默认上下文或调度器,从而导致您想要使用 ConfigureAwait(false)。当然,在这种情况下,如果您避免同步阻塞(在 Web 应用程序中无论如何都应避免这样做),如果您不介意在这种有限情况下的少量性能开销,您可能可以不使用 ConfigureAwait(false)。
在对 IAsyncEnumerable 进行 “await foreach ”时,能否使用 ConfigureAwait?
是的,请参阅 MSDN Magazine 这篇文章中的示例。
await foreach 与一种模式绑定,因此,虽然它可以用于枚举 IAsyncEnumerable<T>,但也可以用于枚举暴露正确 API 表面区域的东西。.NET运行时库包含一个关于 IAsyncEnumerable<T> 的 ConfigureAwait 扩展方法,该方法返回一个封装了 IAsyncEnumerable<T> 和布尔值的自定义类型,并公开了正确的模式。当编译器生成对枚举器的 MoveNextAsync 和 DisposeAsync 方法的调用时,这些调用会指向返回的已配置枚举器结构类型,并以所需的配置方式执行等待。
在 “等待使用” IAsyncDisposable 时,能否使用 ConfigureAwait?
是的,不过有一个小麻烦。
与上一个常见问题中描述的 IAsyncEnumerable<T> 一样,.NET 运行时库在 IAsyncDisposable 上公开了一个 ConfigureAwait 扩展方法,await 使用者只要实现了适当的模式(即公开了一个适当的 DisposeAsync 方法),就会很高兴地使用它:
await using (var c = new MyAsyncDisposableClass().ConfigureAwait(false))
这里的问题是,c 的类型现在不是 MyAsyncDisposableClass,而是 System.Runtime.CompilerServices.ConfiguredAsyncDisposable,也就是从 IAsyncDisposable 上的 ConfigureAwait 扩展方法返回的类型。
要解决这个问题,你需要多写一行:
var c = new MyAsyncDisposableClass();
await using (c.ConfigureAwait(false))
现在,c 的类型又变成了所需的 MyAsyncDisposableClass。这也会增加 c 的作用域;如果这有影响,可以用大括号将整个代码包起来。
我使用了 ConfigureAwait(false),但我的 AsyncLocal 仍在 await 之后流向代码,这是一个错误吗?
不,这是预料之中的。AsyncLocal<T> 数据作为 ExecutionContext 的一部分流动,而 ExecutionContext 与 SynchronizationContext 是分开的。除非您使用 ExecutionContext.SuppressFlow() 显式禁用了 ExecutionContext 流量,否则 ExecutionContext(以及 AsyncLocal<T> 数据)将始终在等待中流动,无论是否使用了 ConfigureAwait 来避免捕获原始的 SynchronizationContext。更多信息,请参阅本博文。
.NET(C#)能否帮助我避免在库中明确使用 ConfigureAwait(false)?
库开发人员有时会对需要使用 ConfigureAwait(false) 表示不满,并要求提供侵入性较小的替代方法。
目前还没有任何替代方案,至少没有内置在语言/编译器/运行时中。不过,关于这种解决方案的建议有很多,例如:
https://github.com/dotnet/csharplang/issues/645
https://github.com/dotnet/csharplang/issues/2542
https://github.com/dotnet/csharplang/issues/2649
https://github.com/dotnet/csharplang/issues/2746
如果这对您很重要,或者您觉得自己有新的有趣的想法,我鼓励您在这些讨论或新的讨论中发表您的想法。