***请求上的 ConfigureAwait(false)

Posted

技术标签:

【中文标题】***请求上的 ConfigureAwait(false)【英文标题】:ConfigureAwait(false) on Top Level Requests 【发布时间】:2015-09-22 10:14:25 【问题描述】:

我正在尝试确定是否应在***请求上使用 ConfigureAwait(false)。从该主题的某种权威中阅读这篇文章: http://blog.stephencleary.com/2012/07/dont-block-on-async-code.html

...他推荐这样的东西:

public async Task<JsonResult> MyControllerAction(...)

    try
    
        var report = await _adapter.GetReportAsync();
        return Json(report, JsonRequestBehavior.AllowGet);
    
    catch (Exception ex)
    
        return Json("myerror", JsonRequestBehavior.AllowGet);  // really slow without configure await
    


public async Task<TodaysActivityRawSummary> GetReportAsync()
           
    var data = await GetData().ConfigureAwait(false);

    return data

...它说要在每个 await except ***调用上使用 ConfigureAwait(false)。 但是,当我这样做时,我的异常需要几秒钟返回给调用者 vs. 使用它并让它立即返回。

调用异步方法的 MVC 控制器操作的最佳做​​法是什么?我应该在控制器本身中使用 ConfigureAwait 还是仅在使用 await 请求数据等的服务调用中使用?如果我不在***调用中使用它,等待几秒钟等待异常似乎有问题。我不需要 HttpContext,而且我看到其他帖子说如果不需要上下文,请始终使用 ConfigureAwait(false)。

更新: 我在调用链中的某处丢失了 ConfigureAwait(false),这导致异常没有立即返回。然而,关于是否应在顶层使用 ConfigureAwait(false) 的问题仍然存在。

【问题讨论】:

你能发布一个最小的、可重现的例子来显示时间差异吗? @Stepen Cleary 我意识到我在链中的某个地方缺少 ConfiguredAwait。很抱歉造成混乱。现在异常马上又回来了。我仍在试图弄清楚为什么应该从顶层省略它。也许我错过了,但我认为博客并没有真正解释这一点。 @StephenCleary 如果你只有一个等待而不是一连串的等待,你也不清楚你会做什么......如果你仍然在顶层使用 ConfigureAwait(false) ......例如推送一些东西到服务巴士上。在这种情况下,您正在调用第三方 api,这是您仅在链中可见的 await。 【参考方案1】:

它是一个高流量的网站吗?一种可能的解释是,当您使用ConfigureAwait(false) 时,您正在经历ThreadPoolstarvation。没有ConfigureAwait(false)await 延续通过AspNetSynchronizationContext.Post 排队,实现归结为this:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask; // the newly-created task is now the last one

这里,ContinueWith 使用没有 TaskContinuationOptions.ExecuteSynchronously(我推测,使延续真正异步并减少低堆栈条件的机会)。因此,它从ThreadPool 获取一个空闲线程来执行继续。从理论上讲,它可能恰好是 await 的先前任务完成的同一个线程,但很可能是不同的线程。

此时,如果 ASP.NET 线程池处于饥饿状态(或必须增长以适应新的线程请求),您可能会遇到延迟。值得一提的是,线程池由两个子池组成:IOCP 线程和工作线程(查看this 和this 了解更多详细信息)。您的 GetReportAsync 操作很可能在 IOCP 线程子池上完成,这似乎不会饿死。 OTOH,ContinueWith 延续在工作线程子池上运行,在您的情况下似乎正在挨饿。

如果一直使用ConfigureAwait(false),则不会发生这种情况。在这种情况下,所有await 延续将在相应的先前任务已结束的相同线程上同步运行,无论是 IOCP 还是工作线程。

您可以比较两种情况下的线程使用情况,有和没有ConfigureAwait(false)。当不使用ConfigureAwait(false) 时,我希望这个数字更大:

catch (Exception ex)

    Log("Total number of threads in use=0",
        Process.GetCurrentProcess().Threads.Count);
    return Json("myerror", JsonRequestBehavior.AllowGet);  // really slow without configure await

您还可以尝试增加 ASP.NET 线程池的大小(用于诊断目的,而不是最终解决方案),看看所描述的场景是否确实是这里的情况:

<configuration>
  <system.web>
    <applicationPool 
        maxConcurrentRequestsPerCPU="6000"
        maxConcurrentThreadsPerCPU="0" 
        requestQueueLimit="6000" />
  </system.web>
</configuration>

更新以解决 cmets:

我意识到我在链中的某个地方缺少 ContinueAwait。现在它 即使顶层没有抛出异常也能正常工作 使用 ConfigureAwait(false)。

这表明您的代码或正在使用的第 3 方库可能正在使用阻塞结构(Task.ResultTask.WaitWaitHandle.WaitOne,可能还添加了一些超时逻辑)。你找过那些吗?试试这个更新底部的Task.Run 建议。此外,我仍然会进行线程计数诊断以排除线程池饥饿/卡顿。

所以你是说如果我在顶层使用 ContinueAwait 我失去了异步的全部好处?

不,我不是这么说的。 async 的全部意义在于避免在等待某事时阻塞线程,而不管ContinueAwait(false) 的附加值如何,都可以实现这一目标。

我的意思是 不使用 ConfigureAwait(false) 可能会引入冗余上下文切换(通常意味着线程切换),如果线程池在它的容量。尽管如此,就服务器可扩展性而言,冗余线程切换仍然优于阻塞线程。

平心而论,using ContinueAwait(false) 也可能导致冗余上下文切换,尤其是在调用链中使用不一致的情况下。

也就是说,ContinueAwait(false) 也经常被误用作针对blocking on asynchronous code 引起的死锁的补救措施。这就是为什么我在上面建议在所有代码库中寻找那些阻塞构造。

但是,关于是否 ConfigureAwait(false) 应该在顶层使用。

我希望 Stephen Cleary 能更好地阐述这一点,这是我的想法。

总会有一些“超***”代码调用您的***代码。例如,对于 UI 应用程序,它是调用 async void 事件处理程序的框架代码。对于 ASP.NET,它是异步控制器的BeginExecute超级***代码负责确保在异步任务完成后,继续(如果有)在正确的同步上下文中运行。 不是您的任务代码的责任。例如,可能根本没有延续,比如使用即发即弃的async void 事件处理程序;为什么要在此类处理程序中恢复上下文?

因此,在您的***方法中,如果您不关心await 延续的上下文,请尽快使用ConfigureAwait(false)

此外,如果您正在使用已知与上下文无关但仍可能不一致地使用 ConfigureAwait(false) 的第 3 方库,您可能希望使用 Task.Run 或类似 WithNoContext 的东西来包装调用。您可以这样做以提前从上下文中获取异步调用链:

var report = await Task.Run(() =>
    _adapter.GetReportAsync()).ConfigureAwait(false);
return Json(report, JsonRequestBehavior.AllowGet);

这将引入一个额外的线程切换,但如果 ConfigureAwait(false)GetReportAsync 或其任何子调用中的使用不一致,则可能会为您节省更多。它还可以作为一种解决方法,用于解决调用链中的那些阻塞构造(如果有的话)导致的潜在死锁。

但是请注意,在 ASP.NET 中,HttpContext.Current 并不是唯一与AspNetSynchronizationContext 一起流动的静态属性。例如,还有Thread.CurrentThread.CurrentCulture。确保你真的不在乎失去上下文。

更新以解决评论:

对于布朗尼积分,也许你可以解释一下 ConfigureAwait(false)...什么上下文没有被保留..它只是 HttpContext还是类对象的局部变量等?

async 方法的所有局部变量都保留在 await 以及隐含的 this 引用中 - 设计使然。它们实际上被捕获到编译器生成的异步状态机结构中,因此从技术上讲,它们并不驻留在当前线程的堆栈中。在某种程度上,它类似于 C# 委托捕获局部变量的方式。实际上,await 延续回调本身就是传递给ICriticalNotifyCompletion.UnsafeOnCompleted 的委托(由正在等待的对象实现;对于Task,它是TaskAwaiter;对于ConfigureAwait,它是ConfiguredTaskAwaitable)。

OTOH,大多数全局状态(静态/TLS 变量、静态类属性)不会自动流经等待。流动的内容取决于特定的同步上下文。在没有一个的情况下(或者当使用ConfigureAwait(false) 时),唯一保留的全局状态是由ExecutionContext 流动的状态。微软的 Stephen Toub 有一篇很棒的帖子:"ExecutionContext vs SynchronizationContext"。他提到了SecurityContextThread.CurrentPrincipal,这对安全至关重要。除此之外,我不知道ExecutionContext 流动的任何官方记录和完整的全球状态属性列表。

您可以查看ExecutionContext.Capture source 以了解更多关于具体流向的信息,但您不应依赖此特定实现。相反,您始终可以使用 Stephen Cleary 的 AsyncLocal(或 .NET 4.6 AsyncLocal&lt;T&gt;)之类的东西创建自己的全局状态流逻辑。

或者,把它发挥到极致,你也可以完全放弃ContinueAwait 并创建一个自定义等待者,例如像这样ContinueOnScope。这将允许精确控制要继续处理的线程/上下文以及要流动的状态。

【讨论】:

至于这个:我仍然不清楚为什么斯蒂芬说不要在顶层使用它。我认为这是因为在顶层你通常会做一些事情使用您异步获得的数据/结果。例如,您将更新 UI,并且希望这发生在具有适当上下文的 UI 线程上。显然,您的 MVC 控制器方法并非如此。 @StephenCleary 可能想跳到这里纠正我。 @KingOfHypocrites,很高兴它有帮助。我忘了一件事,检查你的web.config 中是否有&lt;add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" /&gt;。默认情况下它应该在那里,但值得检查。 感谢您的提示。在注意到此设置丢失后我进行了一些研究,看起来在 .net 4.5 中默认情况下此设置已打开,每 StephenCleary firstbestanswer.com/question/2drgnv/…【参考方案2】:

但是关于是否应在顶层使用 ConfigureAwait(false) 的问题仍然存在。

ConfigureAwait(false) 的经验法则是在方法的其余部分不需要上下文时使用它。

在 ASP.NET 中,“上下文”实际上在任何地方都没有明确定义。它确实包括HttpContext.Current、用户主体和用户文化等内容。

所以,问题真正归结为:“Controller.Json 是否需要 ASP.NET 上下文?” Json 肯定有可能不关心上下文(因为它可以从自己的控制器成员中写入当前响应),但 OTOH 它确实会进行“格式化”,这可能需要恢复用户文化。

我不知道Json 是否需要上下文,但它没有以一种或另一种方式记录,而且通常我假设对 ASP.NET 代码的任何调用都可能取决于上下文。因此,为了安全起见,我不会在控制器代码的顶层使用ConfigureAwait(false)

【讨论】:

感谢您的澄清!

以上是关于***请求上的 ConfigureAwait(false)的主要内容,如果未能解决你的问题,请参考以下文章

ConfigureAwait(false) 与将同步上下文设置为 null

ConfigureAwait(false) 性能测试

为整个项目/dll 设置 ConfigureAwait(false)

ConfigureAwait(false) 不会使延续代码在另一个线程中运行

您可以在没有线程安全的情况下使用 ConfigureAwait(false) 吗?

ConfigureAwait(false) 维护线程身份验证,但默认情况下不