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


561

当您拥有服务器端代码(即某些代码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 SomeAsyncFunctionThatGetsCustomer(id).ConfigureAwait(false);

        // now you are on a different thread!  will that cause problems?
        return customer;
    }
}

Answers:


627

更新: ASP.NET Core没有SynchronizationContext。如果您使用的是ASP.NET Core,则无论是否使用都ConfigureAwait(false)无所谓。

对于ASP.NET“完全”或“经典”之类的东西,其余答案仍然适用。

原始帖子(适用于非Core ASP.NET):

该ASP.NET团队的视频提供了有关async在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方法达到await,该方法将被阻止,但线程将返回线程池。当该方法准备好继续时,将从线程池中抢夺任何线程,并将其用于恢复该方法。

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

在我的MSDN文章SynchronizationContextasync简介博客中,我有更多的背景信息。


23
线程本地存储不会在任何上下文中流动。HttpContext.Current由ASP.NET SynchronizationContext进行流传输,默认情况下,ASP.NET会在您进行流传输await,但是不会由ContinueWith。OTOH,执行上下文(包括安全性限制)是通过C#在CLR中提到的情况下,它由两个流ContinueWithawait(即使你使用ConfigureAwait(false))。
Stephen Cleary 2012年

65
如果C#具有对ConfigureAwait(false)的本地语言支持,那不是很好吗?类似于“ awaitnc”(无上下文等待)。在任何地方键入一个单独的方法调用都非常烦人。:)
NathanAldenSr 2014年

19
@NathanAldenSr:讨论了很多。新关键字的问题在于,ConfigureAwait实际上仅在您等待任务时才有意义,而await对任何“ 等待者”起作用。考虑的其他选项包括:如果在库中,默认行为是否应放弃上下文?还是具有默认上下文行为的编译器设置?这两个都被拒绝了,因为更难阅读代码并告诉它做什么。
斯蒂芬·克利

10
@AnshulNigam:这就是为什么控制器动作需要其上下文的原因。但是动作调用的大多数方法都没有。
斯蒂芬·克利史蒂芬2014年

14
@JonathanRoeder:一般而言,您不需要ConfigureAwait(false)避免基于Result/ Wait的死锁,因为在ASP.NET上,您不应该首先使用Result/ Wait
Stephen Cleary 2014年

131

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

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

这是关于ConfigureAwait方法重要性的更详细的解释(摘自我的博客文章):

当您在等待带有await关键字的方法时,编译器会代表您生成一堆代码。此操作的目的之一是处理与UI(或主)线程的同步。此功能的关键组件是,SynchronizationContext.Current它获取当前线程的同步上下文。 SynchronizationContext.Current根据您所处的环境填充。Task的GetAwaiter方法查找 SynchronizationContext.Current。如果当前同步上下文不为null,则传递给该等待者的继续将被发布回该同步上下文。

当使用阻塞方式使用使用新的异步语言功能的方法时,如果有可用的SynchronizationContext,最终将导致死锁。当您以阻塞方式使用此类方法时(使用Wait方法等待Task或直接从Task的Result属性获取结果),您将同时阻塞主线程。当Task最终在线程池中的该方法内部完成时,它将调用该继续以将其发布回主线程,因为该任务SynchronizationContext.Current可用并被捕获。但是这里有一个问题:UI线程被阻塞,您陷入了死锁!

另外,以下两篇非常适合您的文章正好适合您的问题:

最后,来自Lucian Wischik的一段简短的视频正是关于这个主题的:异步库方法应考虑使用Task.ConfigureAwait(false)

希望这可以帮助。


2
“ Task的GetAwaiter方法将查找SynchronizationContext.Current。如果当前同步上下文不为null,则传递给该等待者的继续将被发布回该同步上下文。” -我得到的印象是,您试图说要Task遍历堆栈以获取SynchronizationContext,这是错误的。在SynchronizationContext被调用的前抓起Task,然后将代码的其余部分继续在SynchronizationContext如果SynchronizationContext.Current不为空。
casperOne

1
@casperOne我打算说同样的话。
tugberk 2012年

8
调用者是否应该负责确保SynchronizationContext.Current是清晰的/或确保在a内调用该库,Task.Run()而不是必须.ConfigureAwait(false)在整个类库中进行编写?
binki 2014年

1
@binki-另一方面:(1)假定在许多应用程序中使用了一个库,因此一次在库中进行一些工作以使其在应用程序上变得更容易是具有成本效益的;(2)大概图书馆的作者知道他编写的代码没有理由要求在原始上下文中继续写,他用这些表达.ConfigureAwait(false)。如果这是默认行为,那么对于库作者来说可能会更容易,但是我认为使正确编写一个库变得有些困难,比使其正确编写一个应用程序变得更困难。
制造商史蒂夫(Steve)2015年

4
图书馆的作者为什么要抚慰消费者?如果消费者想陷入僵局,为什么我要阻止它们?
Quarkly

25

我发现使用ConfigureAwait(false)最大的缺点是线程区域性已还原为系统默认值。如果您已配置区域性,例如...

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

并且您正在将其区域性设置为en-US的服务器上托管,那么您会发现在ConfigureAwait(false)之前被称为CultureInfo。CurrentCulture将返回en-AU,而在您获得en-US之后。即

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

如果您的应用程序在执行需要区域性特定数据格式的操作,那么在使用ConfigureAwait(false)时,您需要注意这一点。


27
.NET的现代版本(我认为从4.6开始?)即使在ConfigureAwait(false)使用时也会在线程之间传播区域性。
Stephen Cleary

1
谢谢(你的)信息。我们确实在使用.net 4.5.2
Mick,

11

我对以下实现有一些一般想法Task

  1. 任务是一次性的,但我们不应该使用using
  2. ConfigureAwait在4.5中引入。Task在4.0中引入。
  3. .NET线程始终用于流上下文(请参阅C#,通过CLR书),但是在默认实现中,Task.ContinueWith它们不会b / c,因此意识到上下文切换非常昂贵,并且默认情况下处于关闭状态。
  4. 问题是图书馆开发人员不应该在乎其客户是否需要上下文流,因此它不应该决定是否在上下文流。
  5. [稍后添加]没有权威的答案和适当的参考,我们一直在为此奋斗,这意味着有人没有正确地完成工作。

我有一些关于该主题的文章,但是除了Tugberk的好回答之外,我的看法是,您应该将所有API都异步化,并理想地传递上下文。由于正在执行异步操作,因此可以简单地使用连续性而不是等待,因此不会造成死锁,因为在库中无需等待,并且可以保持流状态,从而保留上下文(例如HttpContext)。

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


6
1)您可以Task.Dispose根据需要致电;您只是不需要大部分时间。2)Task在.NET 4.0中作为TPL的一部分引入,不需要使用ConfigureAwait;在async添加时,他们重用了现有Task类型,而不是发明新的类型Future
Stephen Cleary 2012年

6
3)您混淆了两种不同类型的“上下文”。即使在Tasks 中,C#中通过CLR提到的“上下文”也总是流动的;由ContinueWith所控制的“上下文” 是SynchronizationContextTaskSchedulerStephen Toub的博客中详细解释了这些不同的上下文。
Stephen Cleary 2012年

21
4)库作者不需要关心其调用者是否需要上下文流,因为每个异步方法都是独立恢复的。因此,如果调用者需要上下文流,则无论库作者是否流过上下文流,他们都可以流过它。
Stephen Cleary 2012年

1
起初,您似乎在抱怨而不是回答问题。然后,您谈论的是“上下文”,除了.Net中有几种上下文,而且还不清楚您在谈论哪个(一个或多个?)。即使您不感到困惑(但我想您是的,我相信没有曾经与Threads融为一体,但不再与s融为一体的上下文ContinueWith()),这也使您的答案难以理解。
svick

1
@StephenCleary是的,lib dev不需要知道,它取决于客户端。我以为我说清楚了,但措辞不清楚。
Aliostad 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.