英文原文:https://dev.to/this-is-learning/understanding-and-using-configureawait-in-asynchronous-programming-2da3
在本文中,我将通过一些代码示例解释什么是ConfigureAwait、为什么正确使用它很重要以及如何使用它。
什么是ConfigureAwait?
ConfigureAwait 是一种可用于任何 Task 或 Task<T> 对象的方法,用于配置等待该任务时await 关键字的行为方式。 它采用一个名为 continueOnCapturedContext 的布尔参数,该参数确定等待之后的延续是否应在与启动任务的上下文相同的上下文中运行。
什么是上下文? 上下文是一个抽象,表示与执行线程关联的某种状态或环境。 例如,在 UI 应用程序中,上下文通常是 UI 线程及其关联的消息循环。 在 ASP.NET 应用程序中,上下文是 HTTP 请求及其关联状态。 上下文也可以是提供某些特定行为的自定义实现。
await 的默认行为是在任务启动时捕获当前上下文,并在任务完成时在同一上下文上恢复继续。 这确保了延续可以访问与原始代码相同的状态和资源。 但是,这种行为也会导致一些问题,例如:
- 死锁:如果捕获的上下文被另一个依赖于任务完成情况的操作阻塞,则可能会发生死锁。 例如,如果 UI 线程正在同步等待需要在 UI 线程上恢复的任务,则会发生死锁。
- 性能:如果捕获的上下文是 UI 线程或线程池线程,则恢复该上下文的延续可能会导致不必要的上下文切换和延迟。 例如,如果 UI 线程正忙于处理其他消息,则在该线程上恢复延续可能比在另一个可用线程上恢复花费更长的时间。
- 可扩展性:如果捕获的上下文处理操作的能力有限,则在该上下文上恢复过多的延续可能会耗尽其资源并降低其响应能力。 例如,如果 ASP.NET 应用程序具有固定数量的线程来处理请求,则在这些线程上恢复过多的延续可能会降低其吞吐量并增加其延迟。
为了避免这些问题,我们可以使用ConfigureAwait(false)来告诉await不要在当前上下文上捕获和恢复,而是在任何可用线程上恢复。 这可以提高性能、可扩展性并避免某些场景中的死锁。 然而,这也意味着我们无法访问原始上下文及其在延续中的状态,因此在这种情况下我们需要小心不要访问任何依赖于上下文的资源或代码。
为什么 ConfigureAwait 很重要?
ConfigureAwait 很重要,因为它允许我们控制等待在不同上下文和场景中的行为。 根据我们的需求和目标,我们可以选择使用ConfigureAwait(true) 或ConfigureAwait(false) 来优化代码的性能、可扩展性或正确性。
作为一般经验法则:
- 当您需要访问原始上下文或其在延续中的状态时,请使用ConfigureAwait(true)(或省略它)。 例如,如果您需要在等待任务后更新 UI 或访问一些依赖于 UI 的资源。
- 当您不需要访问原始上下文或其在延续中的状态时,请使用ConfigureAwait(false)。 例如,如果您正在执行一些不依赖于任何特定上下文的 CPU 密集型或 I/O 密集型工作。
但是,此规则有一些例外和注意事项:
- 如果您正在编写可供不同类型的应用程序(UI 或非 UI)使用的库或可重用组件,则应始终使用ConfigureAwait(false),除非您有充分的理由不这样做。 这样,您就可以避免对消费者的上下文以及他们希望如何使用您的代码施加任何不必要的限制或假设。
- 如果您正在编写 ASP.NET 应用程序(或使用同步上下文的任何其他应用程序),您应该意识到使用ConfigureAwait(false) 可能会更改代码的某些行为和期望。 例如,使用ConfigureAwait(false) 可能会破坏一些依赖于访问 HttpContext.Current 或延续中其他请求特定状态的功能。 您还应该注意不要在控制器或处理程序中混合同步和异步代码,因为即使您使用ConfigureAwait(false),这也可能导致死锁。 有关更多详细信息和最佳实践,请参阅 Stephen Cleary 撰写的这篇文章。
- 如果您正在为异步代码编写单元测试,则应该使用ConfigureAwait(true)(或省略它),除非您有充分的理由不这样做。 这样,您可以确保测试的运行方式与生产代码在其预期上下文中运行的方式相同。 在测试中使用ConfigureAwait(false) 可以隐藏一些仅在某些上下文中才会出现的潜在问题或错误。
如何使用ConfigureAwait?
使用ConfigureAwait 非常简单:只需在等待之前将其附加到任何Task 或Task<T> 对象,并传递 true 或 false 作为参数。 例如:
// Capture and resume on the current context
await DoSomethingAsync().ConfigureAwait(true);
// Don't capture and resume on any available thread
await DoSomethingAsync().ConfigureAwait(false);
请注意,ConfigureAwait 仅影响执行await 后立即继续的方式。 它不会影响代码中的任何后续延续或异步调用。 例如:
// Don't capture and resume on any available thread
await DoSomethingAsync().ConfigureAwait(false);
// Capture and resume on whatever context was current at this point
await DoSomethingElseAsync();
// Don't capture and resume on any available thread
await DoAnotherThingAsync().ConfigureAwait(false);
如果您希望在整个代码中一致地应用ConfigureAwait(false)(建议在库中使用),您可以使用Roslynator 或FxCopAnalyzers 等工具来强制执行此规则并检测任何违规行为。
结论
在本文中,我通过一些代码示例解释了ConfigureAwait是什么、为什么正确使用它很重要以及如何使用它。