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

当你有服务器端代码(例如一些ApiController )和你的函数是异步的 – 所以它们返回Task<SomeObject> – 当你等待函数调用ConfigureAwait(false) ,是否认为是最佳做法?

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

我已经打出了一个我在下面讨论的例子:

 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; } } 

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

对于ASP.NET“完整”或“经典”或其他,这个答案的其余部分仍然适用。

原帖(适用于非核心ASP.NET):

这个由ASP.NET团队制作的视频具有在ASP.NET上使用async的最佳信息。

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

在UI应用程序中,只有一个UI线程需要“同步”回来。

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

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

但是,对于ASP.NET Web Api,如果您的请求在一个线程中进入,并且您正在等待某个函数,并且在您返回ApiController函数的最终结果时调用ConfigureAwait(false),这可能会将您置于不同的线程中。

其实,只是在await可以做到这一点。 一旦你的async方法命中, 方法被阻塞,但线程返回到线程池。 当方法准备好继续时,线程池中的任何线程都被抢走并用于恢复方法。

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

我有更多的背景信息在我的MSDN文章SynchronizationContext和我的async介绍博客文章 。

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

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

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

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

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

另外,这里有两个很棒的文章给你,这些文章正是你的问题:

  • 完美的食谱在脚下拍摄自己 – 使用C#5.0异步语言特性结束死锁
  • 用于HTTP API的异步.NET客户端库以及对异步/等待的不良影响的意识

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

希望这可以帮助。

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

  1. 任务是一次性的,但我们不应该 using
  2. ConfigureAwait是在4.5中引入的。 Task是在4.0中引入的。
  3. .NET线程总是用来传递上下文(请参阅C#通过CLR书),但在Task.ContinueWith的默认实现中,它们没有b / c,实现了上下文切换昂贵,并且默认关闭。
  4. 问题是图书馆开发者不应该关心它的客户是否需要上下文流,因此它不应该决定是否流动上下文。
  5. [后来补]没有权威的回答和适当的参考,我们一直在争吵,这意味着有人没有做好自己的工作。

关于这个问题,我有几个帖子 ,但是除了Tugberk的好答案之外,我还应该把所有的API都变成异步的,理想的情况下就是上下文。 既然你正在做异步,你可以简单地使用continuations而不是等待,因为没有等待在库中完成,你保持流动,所以上下文被保留(如HttpContext),因此不会造成死锁。

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

我使用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)时需要注意这一点。