顶级请求上的ConfigureAwait(false)

时间:2015-07-04 23:37:21

标签: c# asp.net-mvc azure asynchronous async-await

我正在试图弄清楚是否应该在顶级请求上使用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)的问题。

2 个答案:

答案 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线程和工作线程(检查thisthis以获取一些额外的细节)。您的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.ResultTask.WaitWaitHandle.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"。他提到SecurityContextThread.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),只是为了安全起见。