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

Ara*_*ami 517 c# task-parallel-library async-await asp.net-web-api

当你有服务器端代码(即一些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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Ste*_*ary 575

更新: ASP.NET Core没有SynchronizationContext.如果您使用的是ASP.NET Core,则无论您是否使用它都无关紧要ConfigureAwait(false).

对于ASP.NET"Full"或"Classic"等等,本答案的其余部分仍然适用.

原帖(非核心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文章SynchronizationContext和我的async介绍博客文章中有更多背景信息.

  • 如果C#对ConfigureAwait(false)有本机语言支持,那不是很好吗?像'awaitnc'之类的东西(等待没有上下文).在任何地方打出一个单独的方法调用是非常烦人的.:) (61认同)
  • *any*context不会传递线程本地存储.`HttpContext.Current`由ASP.NET`SynchronizationContext`传递,当你'await`时它默认流动,但它不是由'ContinueWith`传递的.OTOH,执行上下文(包括安全限制)是CLR中通过C#提到的上下文,它*由*ContinueWith和`await`两者流动(即使你使用`ConfigureAwait(false)`). (23认同)
  • @NathanAldenSr:讨论了很多.新关键字的问题在于,当你等待*tasks*时,`ConfigureAwait`实际上才有意义,而`await`对任何"等待"都有效.考虑的其他选项是:如果在库中,默认行为是否应该丢弃上下文?或者有默认上下文行为的编译器设置?这两个都被拒绝了,因为只是阅读代码并告诉它做什么更难. (16认同)
  • @JonathanRoeder:一般来说,你不应该需要`ConfigureAwait(false)`来避免基于`Result` /`Wait`的死锁,因为在ASP.NET上你不应该在第一个使用`Result` /`Wait`地点. (14认同)
  • @AnshulNigam:这就是控制器操作需要其上下文的原因.但是动作调用的大多数方法都没有. (10认同)
  • @AnshulNigam:只要不需要请求上下文,就应该使用`ConfigureAwait(false)`. (5认同)
  • @RoyiNamir:当异步操作完成时,来自线程池的线程用于实际完成任务.如果你使用`ConfigureAwait(false)`,那么同一个线程继续执行`async`方法而不输入请求上下文.(顺便说一句,这是一个实现细节;这种行为没有记录). (3认同)
  • "默认情况下"表示"除非您使用`ConfigureAwait(false)`".`HttpContext.Request`没有通过`HttpContext.Current`; `HttpContext.Current`是一个静态属性.另外,请注意`HttpContext`对于多线程使用是不安全的; 如果你使用`ConfigureAwait(false)`,你*可以*访问`HttpContext.Request`,但你肯定*不应该*. (3认同)
  • @MuhammadIqbal:我的建议是从asmx迁移到WebAPI. (3认同)
  • @StephenCleary,但是不太可能不使用请求上下文,因为对于每个请求,每个人都需要发送响应,例如this.Request.CreateResponse(HttpStatusCode.xxx) (2认同)
  • 这个答案中缺少的一件事是全球化和文化.使用ConfigureAwait(false)会丢失web.config system.web/globalization设置.有关详细信息,请参阅下面的答案 (2认同)

tug*_*erk 124

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

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

这里有一个关于ConfigureAwait方法重要性的更详细的解释(我的博客文章引用):

当您在等待具有await关键字的方法时,编译器会代表您生成大量代码.此操作的目的之一是处理与UI(或主)线程的同步.此功能的关键组件是SynchronizationContext.Current获取当前线程的同步上下文. SynchronizationContext.Current根据您所处的环境填充.GetAwaiter任务的方法可以查找 SynchronizationContext.Current.如果当前同步上下文不为null,则传递给该awaiter的延续将返回到该同步上下文.

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

另外,这里有两篇很棒的文章,完全适合您的问题:

最后,Lucian Wischik有一个关于这个主题的精彩视频:异步库方法应该考虑使用Task.ConfigureAwait(false).

希望这可以帮助.

  • 调用者不应该确保`SynchronizationContext.Current`是明确的/或者在`Task.Run()`中调用库而不是必须写`.ConfigureAwait(false)`班级图书馆? (6认同)
  • "Task的GetAwaiter方法查找SynchronizationContext.Current.如果当前同步上下文不为null,则传递给该awaiter的延续将返回到该同步上下文." - 我的印象是你试图说'Task`走遍堆栈以获得`SynchronizationContext`,这是错误的.在调用`Task`之前抓取`SynchronizationContext`,如果`SynchronizationContext.Current`不为null,则继续在`SynchronizationContext`上继续代码. (2认同)
  • @casperOne 我也打算这么说。 (2认同)
  • 图书馆的作者为什么要抚慰消费者?如果消费者想陷入僵局,为什么我要阻止它们? (2认同)

Mic*_*ick 20

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

<system.web>
    <globalization culture="en-AU" uiCulture="en-AU" />    
    ...
Run Code Online (Sandbox Code Playgroud)

并且您将托管在其文化设置为en-US的服务器上,然后您将在ConfigureAwait(false)之前找到CultureInfo.CurrentCulture将返回en-AU并在您获得en-US之后.即

// CultureInfo.CurrentCulture ~ {en-AU}
await xxxx.ConfigureAwait(false);
// CultureInfo.CurrentCulture ~ {en-US}
Run Code Online (Sandbox Code Playgroud)

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

  • 现代版本的.NET(我认为自4.6?)将跨线程传播文化,即使使用`ConfigureAwait(false)`. (21认同)
  • 谢谢(你的)信息。我们确实在使用 .net 4.5.2 (4认同)

Ali*_*tad 11

我对以下方面的实施有一些一般性的想法Task:

  1. 任务是一次性的,但我们不应该使用using.
  2. ConfigureAwait是在4.5中引入的.Task是在4.0中引入的.
  3. .NET线程总是用于流动上下文(请参阅C#通过CLR书)但是在默认实现中Task.ContinueWith它们没有b/c它实现了上下文切换是昂贵的并且它默认关闭.
  4. 问题是库开发人员不应该关心它的客户端是否需要上下文流,因此它不应该决定是否流动上下文.
  5. [后来补充]事实上没有权威的答案和适当的参考,我们继续争取这意味着有人没有完成他们的工作.

我有一些关于这个主题的帖子,但除了Tugberk的好答案之外,我还有一个问题就是你应该把所有API变成异步并理想地流动上下文.由于您正在执行异步操作,因此您可以简单地使用continuation而不是等待,因此不会导致死锁,因为在库中没有等待并且您保持流动以便保留上下文(例如HttpContext).

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

  • 4)库作者不需要关心其调用者是否需要上下文流,因为每个异步方法都是独立恢复的.因此,如果调用者需要上下文流,他们可以流动它,无论库作者是否流动它. (20认同)
  • 1)如果你愿意,可以调用`Task.Dispose`; 你绝对不需要绝大多数时间.2)`Task`作为TPL的一部分在.NET 4.0中引入,它不需要`ConfigureAwait`; 当添加`async`时,他们重用现有的`Task`类型而不是发明一个新的`Future`. (6认同)
  • 3)你混淆了两种不同类型的"背景".通过CLR在C#中提到的"上下文"总是流动,即使在"任务"中也是如此.由`ContinueWith`控制的"上下文"是一个`SynchronizationContext`或`TaskScheduler`.这些不同的背景[详见Stephen Toub的博客](http://blogs.msdn.com/b/pfxteam/archive/2012/06/15/executioncontext-vs-synchronizationcontext.aspx). (6认同)