关于c#:为所有服务器端代码调用ConfigureAwait的最佳实践

Best practice to call ConfigureAwait for all server-side code

当您有服务器端代码(即某些ApiController)并且您的函数是异步的(因此它们返回Task)时,您是否认为最好的做法是随时等待您调用ConfigureAwait(false)的函数?

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

我在下面打了一个例子:

1
2
3
4
5
6
7
8
9
10
11
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核心没有SynchronizationContext。如果您使用的是ASP.NET核心,那么您是否使用ConfigureAwait(false)并不重要。

对于ASP.NET"full"或"classic"或其他内容,此答案的其余部分仍然适用。

原始日志(对于非核心ASP.NET):

ASP.NET团队的这段视频提供了有关在ASP.NET上使用async的最佳信息。

I had read that it is more performant since it doesn't have to switch thread contexts back to the original thread context.

这对于UI应用程序是正确的,因为只有一个UI线程需要"同步"回去。

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

因此,ConfigureAwait(false)不会在ASP.NET中为您保存线程跳转;它会为您保存重新进入请求上下文的过程,但这通常非常快。如果您试图对请求进行少量的并行处理,那么ConfigureAwait(false)可能会很有用,但实际上tpl更适合大多数这些场景。

However, with ASP.NET Web Api, if your request is coming in on one thread, and you await some function and call ConfigureAwait(false) that could potentially put you on a different thread when you are returning the final result of your ApiController function.

实际上,仅仅做一个await就可以做到。当您的async方法命中await时,该方法被阻塞,但线程返回线程池。当方法准备好继续时,将从线程池中抓取任何线程并用于恢复该方法。

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

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


简单回答你的问题:不,你不应该在这样的应用程序级别调用ConfigureAwait(false)

tl;dr版的长答案:如果您编写的库不了解您的客户,也不需要同步上下文(我相信您不应该在库中使用),则应该始终使用ConfigureAwait(false)。否则,库的使用者可能会因以阻塞方式使用异步方法而面临死锁。这取决于具体情况。

下面是关于ConfigureAwait方法重要性的更详细的解释(引自我的博客帖子):

When you are awaiting on a method with await keyword, compiler
generates bunch of code in behalf of you. One of the purposes of this
action is to handle synchronization with the UI (or main) thread. The key
component of this feature is the SynchronizationContext.Current which
gets the synchronization context for the current thread.
SynchronizationContext.Current is populated depending on the
environment you are in. The GetAwaiter method of Task looks up for
SynchronizationContext.Current. If current synchronization context is
not null, the continuation that gets passed to that awaiter will get
posted back to that synchronization context.

When consuming a method, which uses the new asynchronous language
features, in a blocking fashion, you will end up with a deadlock if
you have an available SynchronizationContext. When you are consuming
such methods in a blocking fashion (waiting on the Task with Wait
method or taking the result directly from the Result property of the
Task), you will block the main thread at the same time. When
eventually the Task completes inside that method in the threadpool, it
is going to invoke the continuation to post back to the main thread
because SynchronizationContext.Current is available and captured. But
there is a problem here: the UI thread is blocked and you have a
deadlock!

另外,这里有两篇很好的文章可以回答你的问题:

  • 使用C 5.0异步语言功能的完美方法是让自己陷入困境。
  • 用于HTTP API的异步.NET客户端库以及异步/等待的不良影响的意识

最后,有一个来自LucianWischik的关于这个主题的很好的视频:异步库方法应该考虑使用task.configureawait(false)。

希望这有帮助。


我使用configureAwait(false)找到的最大一个回调是线程区域性恢复为系统默认值。如果您配置了一个区域性,例如…

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

并且您在一个区域性设置为en-us的服务器上进行托管,然后您将发现在configureAwait(false)被称为cultureInfo之前,currentCurance将返回en-au,然后您将获得en-us。即

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

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


我对Task的实施有一些总体看法:

  • 任务是一次性的,但我们不应该使用using
  • 4.5中引入了ConfigureAwaitTask在4.0中引入。
  • .NET线程始终用于流式处理上下文(请参见C_via clr book),但在默认的Task.ContinueWith实现中,它们不是B/C,而是意识到上下文切换很昂贵,默认情况下它是关闭的。
  • 问题在于,库开发人员不应该关心其客户机是否需要上下文流,因此不应该决定是否要流上下文。
  • [稍后补充]事实上,没有权威的答案和适当的参考,我们一直在努力,这意味着有人没有做好他们的工作。
  • 我有一些关于这个主题的文章,但是我的看法——除了TugBerk的好答案之外——是您应该将所有API异步化,并理想地流动上下文。因为您正在进行异步操作,所以您可以简单地使用延续而不是等待,这样就不会导致死锁,因为库中没有进行等待,并且您保持流动,这样就可以保留上下文(如httpcontext)。

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