***请求上的 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)
时,您正在经历ThreadPool
starvation。没有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.Result
、Task.Wait
、WaitHandle.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"。他提到了SecurityContext
和Thread.CurrentPrincipal
,这对安全至关重要。除此之外,我不知道ExecutionContext
流动的任何官方记录和完整的全球状态属性列表。
您可以查看ExecutionContext.Capture
source 以了解更多关于具体流向的信息,但您不应依赖此特定实现。相反,您始终可以使用 Stephen Cleary 的 AsyncLocal
(或 .NET 4.6 AsyncLocal<T>
)之类的东西创建自己的全局状态流逻辑。
或者,把它发挥到极致,你也可以完全放弃ContinueAwait
并创建一个自定义等待者,例如像这样ContinueOnScope
。这将允许精确控制要继续处理的线程/上下文以及要流动的状态。
【讨论】:
至于这个:我仍然不清楚为什么斯蒂芬说不要在顶层使用它。我认为这是因为在顶层你通常会做一些事情使用您异步获得的数据/结果。例如,您将更新 UI,并且希望这发生在具有适当上下文的 UI 线程上。显然,您的 MVC 控制器方法并非如此。 @StephenCleary 可能想跳到这里纠正我。 @KingOfHypocrites,很高兴它有帮助。我忘了一件事,检查你的web.config
中是否有<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
。默认情况下它应该在那里,但值得检查。
感谢您的提示。在注意到此设置丢失后我进行了一些研究,看起来在 .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
为整个项目/dll 设置 ConfigureAwait(false)
ConfigureAwait(false) 不会使延续代码在另一个线程中运行