ChatGPT解决这个技术问题 Extra ChatGPT

为所有服务器端代码调用 ConfigureAwait 的最佳实践

当您有服务器端代码(即一些 ApiController)并且您的函数是异步的 - 所以它们返回 Task<SomeObject> - 任何时候等待调用 ConfigureAwait(false) 的函数是否被认为是最佳实践?

我读过它的性能更高,因为它不必将线程上下文切换回原始线程上下文。但是,对于 ASP.NET Web Api,如果您的请求来自一个线程,并且您等待某个函数并调用 ConfigureAwait(false),当您返回 ApiController 的最终结果时,这可能会将您置于另一个线程功能。

我在下面输入了一个我正在谈论的示例:

public class CustomerController : ApiController
{
    public async Task<Customer> Get(int id)
    {
        // you are on a particular thread here
        var customer = await GetCustomerAsync(id).ConfigureAwait(false);
        
        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}

s
stil

更新: ASP.NET Core does not have a SynchronizationContext。如果您使用的是 ASP.NET Core,那么是否使用 ConfigureAwait(false) 都没有关系。

对于 ASP.NET “完整”或“经典”或其他任何内容,此答案的其余部分仍然适用。

原始帖子(针对非核心 ASP.NET):

This video by the ASP.NET team has the best information on using async on ASP.NET.

我读过它的性能更高,因为它不必将线程上下文切换回原始线程上下文。

对于 UI 应用程序也是如此,其中只有一个 UI 线程需要“同步”回。

在 ASP.NET 中,情况要复杂一些。当 async 方法恢复执行时,它会从 ASP.NET 线程池中获取一个线程。如果您使用 ConfigureAwait(false) 禁用上下文捕获,那么线程将继续直接执行该方法。如果不禁用上下文捕获,那么线程将重新进入请求上下文,然后继续执行该方法。

所以 ConfigureAwait(false) 不会为您节省 ASP.NET 中的线程跳转;它确实为您节省了重新输入请求上下文的时间,但这通常非常快。 ConfigureAwait(false)如果您尝试对请求进行少量并行处理,可能很有用,但实际上 TPL 更适合大多数情况。

但是,对于 ASP.NET Web Api,如果您的请求来自一个线程,并且您等待某个函数并调用 ConfigureAwait(false),当您返回 ApiController 函数的最终结果时,这可能会将您置于另一个线程.

实际上,只需执行 await 即可。一旦您的 async 方法遇到 awaitmethod 就会被阻塞,但 thread 会返回到线程池。当方法准备好继续时,从线程池中抢夺任何线程并用于恢复该方法。

ConfigureAwait 在 ASP.NET 中的唯一区别是该线程在恢复方法时是否进入请求上下文。

我的 MSDN article on SynchronizationContextasync intro blog post 中有更多背景信息。


any 上下文不流动线程本地存储。 HttpContext.Current 由 ASP.NET SynchronizationContext 流动,当您 await 时默认流动,但不由 ContinueWith 流动。 OTOH,执行上下文(包括安全限制)是在 CLR 中通过 C# 提到的上下文,它ContinueWithawait 流动(即使您使用 ConfigureAwait(false))。
如果 C# 具有对 ConfigureAwait(false) 的本地语言支持,那不是很好吗?像'awaitnc'(等待没有上下文)之类的东西。到处打出一个单独的方法调用是很烦人的。 :)
@NathanAldenSr:讨论了很多。新关键字的问题在于 ConfigureAwait 实际上仅在您等待 tasks 时才有意义,而 await 作用于任何“等待”。其他考虑的选项是:如果在库中,默认行为是否应该丢弃上下文?或者有默认上下文行为的编译器设置?这两个都被拒绝了,因为很难仅仅阅读代码并说出它的作用。
@AnshulNigam:这就是控制器动作需要它们的上下文的原因。但是动作调用的大多数方法都没有。
@JonathanRoeder:一般来说,您不需要 ConfigureAwait(false) 来避免基于 Result/Wait 的死锁,因为在 ASP.NET 上,您首先不应该使用 Result/Wait
t
tugberk

简要回答您的问题:不。您不应该像那样在应用程序级别调用 ConfigureAwait(false)

TL;DR 版本的长答案:如果您正在编写一个不了解您的使用者并且不需要同步上下文的库(我相信您不应该在库中),您应该始终使用 {1 }。否则,您的库的使用者可能会因为以阻塞方式使用您的异步方法而面临死锁。这取决于情况。

以下是关于 ConfigureAwait 方法重要性的更详细说明(引用自我的博客文章):

当您使用 await 关键字等待方法时,编译器会代表您生成一堆代码。此操作的目的之一是处理与 UI(或主)线程的同步。此功能的关键组件是 SynchronizationContext.Current,它获取当前线程的同步上下文。根据您所处的环境填充 SynchronizationContext.Current。Task 的 GetAwaiter 方法查找 SynchronizationContext.Current。如果当前同步上下文不为空,则传递给该等待者的延续将被回发到该同步上下文。当以阻塞方式使用使用新异步语言特性的方法时,如果您有可用的 SynchronizationContext,您将最终陷入死锁。当您以阻塞方式使用此类方法时(使用 Wait 方法等待 Task 或直接从 Task 的 Result 属性中获取结果),您将同时阻塞主线程。当任务最终在线程池中的该方法内完成时,它将调用延续以回发到主线程,因为 SynchronizationContext.Current 可用并被捕获。但是这里有个问题:UI线程被阻塞了,你就死锁了!

此外,这里有两篇非常适合您的文章,它们正是针对您的问题:

用 C# 5.0 异步语言特性解决死锁的完美秘诀

用于 HTTP API 的异步 .NET 客户端库以及对 async/await 不良影响的认识

最后,Lucian Wischik 提供了一个非常棒的短视频,正是关于这个主题的:Async library methods should consider using Task.ConfigureAwait(false)

希望这可以帮助。


“Task 的 GetAwaiter 方法查找 SynchronizationContext.Current。如果当前同步上下文不为空,则传递给该等待者的延续将被发送回该同步上下文。” - 我的印象是您试图说 Task 遍历堆栈以获取 SynchronizationContext,这是错误的。 SynchronizationContext 在调用 Task 之前被抓取,然后如果 SynchronizationContext.Current 不为 null,则其余代码在 SynchronizationContext 上继续。
@casperOne 我也打算这么说。
是否应该由调用者负责确保 SynchronizationContext.Current 清晰/或者在 Task.Run() 内调用库而不是在整个类库中编写 .ConfigureAwait(false)
@binki - 另一方面:(1)可能在许多应用程序中都使用了一个库,因此在库中一次性努力使应用程序更容易是具有成本效益的; (2) 大概库作者知道他编写的代码没有理由要求继续原始上下文,他用那些 .ConfigureAwait(false) 来表达。如果这是默认行为,库作者可能会更容易,但我认为让正确编写库变得更难一点比让正确编写应用程序更难一点要好。
为什么图书馆的作者要溺爱消费者?如果消费者想要死锁,我为什么要阻止它们?
M
Mick

我发现使用 ConfigureAwait(false) 的最大缺点是线程文化恢复为系统默认值。如果您配置了一种文化,例如...

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...

并且您托管在其文化设置为 en-US 的服务器上,那么您会在 ConfigureAwait(false) 被调用之前发现 CultureInfo.CurrentCulture 将返回 en-AU 并且在您将获得 en-US 之后。 IE

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}

如果您的应用程序正在执行任何需要对数据进行文化特定格式的操作,那么在使用 ConfigureAwait(false) 时需要注意这一点。


.NET 的现代版本(我认为是从 4.6 开始?)将跨线程传播文化,即使使用 ConfigureAwait(false)
谢谢(你的)信息。我们确实在使用 .net 4.5.2
B
Ben Collins

我对 Task 的实现有一些一般性的想法:

任务是一次性的,但我们不应该使用 using。 ConfigureAwait 是在 4.5 中引入的。任务是在 4.0 中引入的。 .NET 线程总是用于流动上下文(参见 C# via CLR 书),但在 Task.ContinueWith 的默认实现中,它们不是 b/c,它意识到上下文切换是昂贵的,默认情况下它是关闭的。问题是库开发人员不应该关心其客户是否需要上下文流,因此它不应该决定是否流上下文。 [稍后添加] 没有权威的答案和适当的参考,我们一直在为此奋斗,这意味着有人没有做好他们的工作。

我有几个关于这个主题的posts,但我的看法 - 除了 Tugberk 的好答案 - 您应该将所有 API 变为异步并理想地流动上下文。 由于您正在执行异步,您可以简单地使用延续而不是等待,因此不会导致死锁,因为库中没有等待,并且您保持流动,因此保留了上下文(例如 HttpContext)。

问题是库公开了同步 API 但使用了另一个异步 API - 因此您需要在代码中使用 Wait()/Result


1) 如果需要,您可以调用 Task.Dispose;你只是不需要绝大多数时间。 2) Task 作为 TPL 的一部分在 .NET 4.0 中引入,不需要 ConfigureAwait;添加 async 后,他们重用了现有的 Task 类型,而不是发明了新的 Future
3)您混淆了两种不同类型的“上下文”。 C# 中通过 CLR 提到的“上下文”总是流动的,即使在 Tasks 中也是如此; ContinueWith 控制的“上下文”是 SynchronizationContextTaskScheduler。这些不同的上下文are explained in detail on Stephen Toub's blog
4)库作者不需要关心它的调用者是否需要上下文流,因为每个异步方法都是独立恢复的。因此,如果调用者需要上下文流,他们可以对其进行处理,而不管库作者是否对其进行了处理。
起初,您似乎在抱怨而不是回答问题。然后你在谈论“上下文”,除了.Net 中有几种上下文,而且你说的是哪一种(或几种?)真的不清楚。即使您自己并不感到困惑(但我认为您是,我相信没有过去与 Thread 一起流动的上下文,但不再与 ContinueWith() 一起流动),这会使您的答案难以阅读.
@StephenCleary 是的,lib dev 不需要知道,这取决于客户端。我以为我说清楚了,但我的措辞不清楚。