我正在试图弄清楚是否应该在顶级请求上使用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 上使用ConfigureAwait(false),除了顶级调用。 但是在执行此操作时,我的异常需要几秒钟才能完成返回呼叫者与使用它并让它立即回来。
调用异步方法的MVC控制器操作的最佳实践是什么?我应该在控制器本身中使用ConfigureAwait,还是仅在使用等待请求数据等的服务调用中使用?如果我不在顶级调用中使用它,等待几秒钟的异常似乎有问题。我不需要HttpContext,如果你不需要上下文,我看过其他帖子总是使用ConfigureAwait(false)。
更新 我在我的调用链中某处缺少ConfigureAwait(false),这导致异常不会立即返回。然而,问题仍然是关于是否应该在顶层使用ConfigureAwait(false)的问题。
答案 0 :(得分:28)
这是一个高流量的网站吗?一种可能的解释可能是,当您不使用ThreadPool
时,您会遇到ConfigureAwait(false)
饥饿。如果没有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>
更新了以解决评论:
我意识到我在链条的某个地方错过了一个ContinueAwait。现在它 即使最高级别没有,也可以在抛出异常时正常工作 使用ConfigureAwait(false)。
这表明您的代码或正在使用的第三方库可能正在使用阻塞构造(Task.Result
,Task.Wait
,WaitHandle.WaitOne
,可能还有一些添加的超时逻辑)。你找那些了吗?请尝试此更新底部的Task.Run
建议。此外,我仍然进行线程计数诊断以排除线程池饥饿/口吃。
所以你是说如果我在最顶层使用ContinueAwait 我失去了异步的全部好处?
不,我不这么说。 async
的重点是避免在等待某事时阻塞线程,无论ContinueAwait(false)
的附加值如何,都可以实现这一目标。
我所说的是不使用 ConfigureAwait(false)
可能会引入冗余上下文切换(通常意味着线程切换),如果线程可能是ASP.NET中的问题游泳池正在运作。尽管如此,就服务器可伸缩性而言,冗余线程切换仍然比阻塞线程更好。
平心而论,using ContinueAwait(false)
也可能导致冗余的上下文切换,特别是如果它在调用链中使用不一致的话。
尽管如此,ContinueAwait(false)
也经常被误用作对blocking on asynchronous code引起的死锁的补救措施。这就是我上面建议在所有代码库中查找阻塞构造的原因。
然而,问题仍然存在,无论是否 应该在顶层使用ConfigureAwait(false)。
我希望Stephen Cleary可以通过这里的想法更好地阐述这一点。
总有一些&#34;超顶级&#34;调用顶级代码的代码。例如,在UI应用程序的情况下,它是调用async void
事件处理程序的框架代码。对于ASP.NET,它是异步控制器的BeginExecute
。超级顶级代码的责任是确保在异步任务完成后,继续(如果有)在正确的同步上下文上运行。 不是您的任务代码的责任。例如,可能根本没有延续,例如使用即发消息async void
事件处理程序;你为什么要关心在这样的处理程序中恢复上下文?
因此,在您的顶级方法中,如果您不关心await
延续的上下文,请尽快使用ConfigureAwait(false)
。
此外,如果您使用已知与上下文无关但仍可能不一致地使用ConfigureAwait(false)
的第三方库,您可能希望使用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
流动的静态属性。例如,还有async
。确保你真的不在乎失去上下文。
更新了以解决评论:
对于布朗尼点,也许你可以解释一下 ConfigureAwait(false)...什么上下文没有被保留..它只是 HttpContext或类对象的局部变量等?
await
方法的所有局部变量都保留在this
和隐式await
引用 - 按设计保留。它们实际上被捕获到编译器生成的异步状态机结构中,因此从技术上讲它们不会驻留在当前线程的堆栈中。在某种程度上,它类似于C#委托捕获局部变量的方式。实际上,ICriticalNotifyCompletion.UnsafeOnCompleted
延续回调本身就是传递给Task
的委托(由等待的对象实现;对于ConfigureAwait
,它是TaskAwaiter
;使用{ {1}},它是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
。这样就可以精确控制继续使用的线程/上下文以及要流动的状态。
答案 1 :(得分:20)
然而,仍然存在问题,即是否应该在顶层使用ConfigureAwait(false)。
ConfigureAwait(false)
的经验法则是,只要方法的其余部分不需要上下文,就可以使用它。
在ASP.NET中,“上下文”实际上并未在任何地方定义明确。它包括HttpContext.Current
,用户主体和用户文化等内容。
所以,问题实际上归结为:“Controller.Json
是否需要ASP.NET上下文?” Json
当然不关心上下文(因为它可以从其自己的控制器成员写入当前响应),但OTOH确实可以“格式化”,这可能需要恢复用户文化。
我不知道Json
是否需要上下文,但它没有以某种方式记录,并且通常我认为对ASP.NET代码的任何调用都可能取决于上下文。所以我不在我的控制器代码的顶层使用ConfigureAwait(false)
,只是为了安全起见。