首页 文章

顶级请求上的ConfigureAwait(false)

提问于
浏览
27

我想弄清楚是否应该在顶级请求上使用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 回答

  • 27

    这是一个高流量的网站吗?一种可能的解释可能是,当您不使用 ConfigureAwait(false) 时,您正在经历 ThreadPool 饥饿 . 如果没有 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 continuation将在相应的先前任务结束的相同线程上同步运行,无论是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>
    

    Updated 发表评论:

    我意识到我在链条的某个地方错过了一个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能够更好地阐述这一点,这是我的想法 .

    有'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' s BeginExecute . 超级顶级代码负责确保在异步任务完成后,继续(如果有)在正确的同步上下文上运行 . 这不是您的任务代码的责任 . 例如,可能根本没有延续,例如使用“即发即忘” async void 事件处理程序;你为什么要关心在这样的处理程序中恢复上下文?

    因此,在顶级方法中,如果您不关心 await continuation的上下文,请尽快使用 ConfigureAwait(false) .

    此外,如果您使用的第三方库已知与上下文无关,但仍可能使用 ConfigureAwait(false) 不一致,您可能希望使用 Task.Run 或类似WithNoContext之类的方式包含调用 . 你这样做是为了让异步调用的链从事物中提前关闭:

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

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

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

    Updated 发表评论:

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

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

    OTOH,大多数 global state (静态/ TLS变量,静态类属性)都是 not 自动流过等待 . 流动的内容取决于特定的同步上下文 . 如果没有一个(或者当使用 ConfigureAwait(false) 时),保留的唯一全局状态是由 ExecutionContext 流动的 . 微软的Stephen Toub有一篇很棒的帖子:"ExecutionContext vs SynchronizationContext" . 他提到 SecurityContextThread.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 . 这将允许精确控制要继续的线程/上下文以及要流动的状态 .

  • 18

    但是问题仍然存在,是否应该在顶层使用ConfigureAwait(false) .

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

    在ASP.NET中,"context"实际上并未在任何地方定义良好 . 它包括 HttpContext.Current ,用户主体和用户文化等内容 .

    所以,问题实际上归结为:“ Controller.Json 需要ASP.NET上下文吗?”当然 Json 可能不关心上下文(因为它可以从自己的控制器成员写入当前响应),但OTOH确实可以做"formatting",这可能需要恢复用户文化 .

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

相关问题