我想弄清楚是否应该在顶级请求上使用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
}
...it says to using ConfigureAwait(false) on every await except the top level call. 然而,在执行此操作时,我的异常需要几秒钟才能返回调用者而不是使用它并让它立即返回 .
调用异步方法的MVC控制器操作的最佳实践是什么?我应该在控制器本身中使用ConfigureAwait,还是只在使用等待请求数据等的服务调用中使用?如果我不在顶级调用中使用它,等待几秒钟的异常似乎有问题 . 我不需要HttpContext,并且我已经看过其他帖子,如果你不需要上下文,那么总是使用ConfigureAwait(false) .
Update: 我的调用链中某处缺少ConfigureAwait(false),导致异常无法立即返回 . 但是问题仍然存在,是否应该在顶层使用ConfigureAwait(false) .
2 回答
这是一个高流量的网站吗?一种可能的解释可能是,当您不使用
ConfigureAwait(false)
时,您正在经历ThreadPool
饥饿 . 如果没有ConfigureAwait(false)
,await
延续将通过AspNetSynchronizationContext.Post
排队,该实现归结为this:这里使用
ContinueWith
而没有TaskContinuationOptions.ExecuteSynchronously
(我推测,使延续真正异步并减少低堆栈条件的机会) . 因此,它从ThreadPool
获取一个空的线程来执行继续 . 从理论上讲,它可能恰好是await
的先行任务完成的同一个线程,但很可能它是一个不同的线程 .此时,如果ASP.NET线程池正在挨饿(或者必须增长以容纳新的线程请求),则可能会遇到延迟 . 值得一提的是,线程池由两个子池组成:IOCP线程和工作线程(检查this和this以获取一些额外的细节) . 您的
GetReportAsync
操作很可能在IOCP线程子池上完成,该子池似乎没有挨饿 . OTOH,ContinueWith
延续在一个工作线程子池上运行,它似乎在您的情况下挨饿 .如果始终使用
ConfigureAwait(false)
,则不会发生这种情况 . 在这种情况下,所有await
continuation将在相应的先前任务结束的相同线程上同步运行,无论是IOCP还是工作线程 .您可以比较两种方案的线程使用情况,包括和不使用
ConfigureAwait(false)
. 当不使用ConfigureAwait(false)
时,我希望这个数字更大:您还可以尝试增加ASP.NET线程池的大小(用于诊断目的,而不是最终解决方案),以查看所描述的场景是否确实如此:
Updated 发表评论:
这表明您的代码或正在使用的第三方库可能正在使用阻塞构造(
Task.Result
,Task.Wait
,WaitHandle.WaitOne
,可能还有一些添加的超时逻辑) . 你找那些了吗?尝试从此更新底部的Task.Run
建议 . 此外,我仍然会进行线程计数诊断以排除线程池饥饿/口吃 .不,我不是这么说的 .
async
的重点是避免在等待某事时阻塞线程,无论ContinueAwait(false)
的附加值如何,都可以实现该目标 .我所说的是,不使用
ConfigureAwait(false)
可能会引入冗余上下文切换(通常意味着线程切换),如果线程池正在以其容量工作,这可能是ASP.NET中的一个问题 . 然而,就服务器可扩展性而言,冗余线程切换仍然比阻塞线程更好 .平心而论,using ContinueAwait(false)也可能导致冗余的上下文切换,特别是如果它在调用链中使用不一致 .
也就是说,
ContinueAwait(false)
也经常被误用作对由blocking on asynchronous code引起的死锁的补救措施 . 这就是为什么我在上面建议在所有代码库中寻找那些阻塞构造的原因 .我希望Stephen Cleary能够更好地阐述这一点,这是我的想法 .
有's always some 1289848 code that invokes your top-level code. E.g., in case of a UI app, it'是调用
async void
事件处理程序的框架代码 . 在ASP.NET的情况下,它's the asynchronous controller' sBeginExecute
. 超级顶级代码负责确保在异步任务完成后,继续(如果有)在正确的同步上下文上运行 . 这不是您的任务代码的责任 . 例如,可能根本没有延续,例如使用“即发即忘”async void
事件处理程序;你为什么要关心在这样的处理程序中恢复上下文?因此,在顶级方法中,如果您不关心
await
continuation的上下文,请尽快使用ConfigureAwait(false)
.此外,如果您使用的第三方库已知与上下文无关,但仍可能使用
ConfigureAwait(false)
不一致,您可能希望使用Task.Run
或类似WithNoContext之类的方式包含调用 . 你这样做是为了让异步调用的链从事物中提前关闭:这将引入一个额外的线程切换,但如果在
GetReportAsync
或其任何子调用中使用ConfigureAwait(false)
不一致,可能会为您节省更多 . 它还可以作为潜在死锁的解决方法,这些死锁是由调用链中的阻塞构造(如果有的话)引起的 .但请注意,在ASP.NET中,
HttpContext.Current
不是唯一使用AspNetSynchronizationContext
流动的静态属性 . 例如,还有Thread.CurrentThread.CurrentCulture
. 确保你真的不关心失去上下文 .Updated 发表评论:
async
方法的所有局部变量都保留在await
之间,以及隐式this
引用 - 按设计保留 . 它们实际上被捕获到编译器生成的异步状态机结构中,因此从技术上讲它们不会堆叠 . 在某种程度上,它类似于C#委托如何捕获局部变量 . 事实上,await
延续回调本身就是传递给ICriticalNotifyCompletion.UnsafeOnCompleted
的委托(由等待的对象实现;对于Task
,它是TaskAwaiter;有ConfigureAwait
,它是ConfiguredTaskAwaitable) .OTOH,大多数 global state (静态/ TLS变量,静态类属性)都是 not 自动流过等待 . 流动的内容取决于特定的同步上下文 . 如果没有一个(或者当使用
ConfigureAwait(false)
时),保留的唯一全局状态是由ExecutionContext
流动的 . 微软的Stephen Toub有一篇很棒的帖子:"ExecutionContext vs SynchronizationContext" . 他提到SecurityContext
和Thread.CurrentPrincipal
,这对安全至关重要 . 除此之外,我不知道由ExecutionContext
流动的任何官方文件和完整的全球州 property 清单 .您可以查看ExecutionContext.Capture source以了解更多关于究竟流动的内容,但您不应该't depend on this specific implementation. Instead, you can always create your own global state flow logic, using something like Stephen Cleary'(或.NET 4.6 AsyncLocal<T>) .
或者,为了将它发挥到极致,你也可以完全抛弃
ContinueAwait
并创建一个自定义等待者,例如喜欢这个ContinueOnScope . 这将允许精确控制要继续的线程/上下文以及要流动的状态 .ConfigureAwait(false)
的经验法则是在方法的其余部分不需要上下文时使用它 .在ASP.NET中,"context"实际上并未在任何地方定义良好 . 它包括
HttpContext.Current
,用户主体和用户文化等内容 .所以,问题实际上归结为:“
Controller.Json
需要ASP.NET上下文吗?”当然Json
可能不关心上下文(因为它可以从自己的控制器成员写入当前响应),但OTOH确实可以做"formatting",这可能需要恢复用户文化 .我不知道
Json
是否需要上下文,但它没有以某种方式记录,并且通常我假设对ASP.NET代码的任何调用都可能取决于上下文 . 所以我不会在我的控制器代码的顶层使用ConfigureAwait(false)
,只是为了安全起见 .