【问题标题】:Best practice to call ConfigureAwait for all server-side code为所有服务器端代码调用 ConfigureAwait 的最佳实践
【发布时间】:2012-11-09 10:57:19
【问题描述】:

当您有服务器端代码(即一些 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;
    }
}

【问题讨论】:

    标签: c# asp.net-web-api task-parallel-library async-await


    【解决方案1】:

    更新: 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 方法遇到await方法 将被阻塞,但线程 返回到线程池。当方法准备好继续时,从线程池中抢夺任何线程并用于恢复方法。

    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/@首先是 987654351@。
    【解决方案2】:

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

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

    这里对ConfigureAwait 方法的重要性进行了更详细的解释(引用自我的博客文章):

    当你在等待一个带有 await 关键字的方法时,编译器 代表你生成一堆代码。这样做的目的之一 操作是处理与 UI(或主)线程的同步。钥匙 此功能的组成部分是SynchronizationContext.Current,它 获取当前线程的同步上下文。 SynchronizationContext.Current 的填充取决于 您所在的环境。Task 的 GetAwaiter 方法查找 SynchronizationContext.Current。如果当前同步上下文是 不为空,传递给该等待者的延续将得到 回发到该同步上下文。

    使用新的异步语言的方法时 功能,以阻塞的方式,你最终会陷入僵局,如果 你有一个可用的 SynchronizationContext。当你在消费 这样的方法以阻塞方式(等待任务等待 方法或直接从 Result 的属性中获取结果 任务),您将同时阻塞主线程。什么时候 最终任务在线程池中的该方法内完成,它 将调用延续回发到主线程 因为SynchronizationContext.Current 可用并被捕获。但 这里有一个问题:UI线程被阻塞了,你有一个 死锁!

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

    最后,Lucian Wischik 有一个关于这个主题的精彩短视频:Async library methods should consider using Task.ConfigureAwait(false)

    希望这会有所帮助。

    【讨论】:

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

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

    【讨论】:

    • .NET 的现代版本(我认为是从 4.​​6 开始?)将跨线程传播文化,即使使用了 ConfigureAwait(false)
    • 感谢您的信息。我们确实在使用 .net 4.5.2
    【解决方案4】:

    我对@9​​87654324@的实现有一些大致的想法:

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

    我有几个 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 中有几种上下文,而且你说的是哪一种(或几种?)真的不清楚。即使您自己并不感到困惑(但我认为您是,我相信没有过去与Threads 一起流动的上下文,但不再与ContinueWith() 一起流动),这会使您的答案难以阅读.
    • @StephenCleary 是的,lib dev 不需要知道,这取决于客户端。我以为我说得很清楚,但我的措辞不清楚。
    猜你喜欢
    • 1970-01-01
    • 2010-12-21
    • 2020-07-20
    • 2012-05-24
    • 2018-03-06
    • 1970-01-01
    • 2020-08-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多