Async Void、ASP.Net 和未完成操作计数

Posted

技术标签:

【中文标题】Async Void、ASP.Net 和未完成操作计数【英文标题】:Async Void, ASP.Net, and Count of Outstanding Operations 【发布时间】:2013-07-13 15:13:03 【问题描述】:

我试图了解为什么 ASP.Net 应用程序中的 async void 方法会导致以下异常,而 async Task 似乎不会:

System.InvalidOperationException: An asynchronous module or handler 
completed while an asynchronous operation was still pending

我对 .NET 中的异步世界比较陌生,但我确实觉得我已经尝试通过许多现有资源来运行这个,包括以下所有资源:

What's the difference between returning void and returning a Task? It's All About the SynchronizationContext Async Syntactic Sugar Suggestions Async in ASP.NET

从这些资源中,我了解到最佳做法是通常返回 Task 并避免异步 void。我也明白 async void 在调用方法时会增加未完成操作的计数,并在完成时减少它。这听起来至少是我问题的部分答案。但是,我缺少的是当我返回 Task 时会发生什么以及为什么这样做会使事情“工作”。

这是一个人为的例子来进一步说明我的问题:

public class HomeController : AsyncController

    // This method will work fine
    public async Task<ActionResult> ThisPageWillLoad()
    
        // Do not await the task since it is meant to be fire and forget
        var task = this.FireAndForgetTask();

        return await Task.FromResult(this.View("Index"));
    

    private async Task FireAndForgetTask()
    
        var task = Task.Delay(TimeSpan.FromSeconds(3));
        await task;
    

    // This method will throw the following exception:
    // System.InvalidOperationException: An asynchronous module or 
    // handler completed while an asynchronous operation was still pending
    public async Task<ActionResult> ThisPageWillNotLoad()
    
        // Obviously can't await a void method
        this.FireAndForgetVoid();

        return await Task.FromResult(this.View("Index"));
    

    private async void FireAndForgetVoid()
    
        var task = Task.Delay(TimeSpan.FromSeconds(3));
        await task;
    

在相关说明中,如果我对 async void 的理解是正确的,那么在这种情况下将 async void 视为“即发即弃”是不是有点错误,因为 ASP.Net 实际上并没有忘记它?

【问题讨论】:

async void 是“一劳永逸”,因为你不能 await 它,因为它不会返回等待的 Task 【参考方案1】:

Microsoft 决定在将async 引入 ASP.NET 时尽可能避免出现向后兼容性问题。他们想把它带到他们所有的“一个 ASP.NET”——所以async 支持 WinForms、MVC、WebAPI、SignalR 等。

从历史上看,自 .NET 2.0 起,ASP.NET 通过基于事件的异步模式 (EAP) 支持干净的异步操作,其中异步组件通知 SynchronizationContext 其开始和完成。 .NET 4.5 对此支持进行了第一次相当大的更改,更新了核心 ASP.NET 异步类型以更好地启用基于任务的异步模式(TAP,即async)。

与此同时,每个不同的框架(WebForms、MVC 等)都开发了自己的方式来与该核心交互,向后兼容性是一个优先事项。为了帮助开发人员,核心 ASP.NET SynchronizationContext 得到了增强,但出现了异常;它会发现很多使用错误。

在 WebForms 世界中,他们有 RegisterAsyncTask,但很多人只是使用 async void 事件处理程序。因此,ASP.NET SynchronizationContext 将在页面生命周期的适当时间允许 async void,如果您在不适当的时间使用它,则会引发该异常。

在 MVC/WebAPI/SignalR 世界中,框架更加结构化为服务。所以他们能够以一种非常自然的方式采用async Task,而框架只需要处理返回的Task——一个非常干净的抽象。作为旁注,您不再需要AsyncController; MVC 知道它是异步的,因为它返回一个 Task

但是,如果您尝试返回 Task并且使用 async void,则不支持。几乎没有理由支持它。仅仅支持那些不应该这样做的用户将是相当复杂的。请记住,async void 直接通知核心 ASP.NET SynchronizationContext,完全绕过了 MVC 框架。 MVC 框架了解如何等待您的 Task,但它甚至不知道 async void,因此它会将完成返回给 ASP.NET 核心,它发现它实际上不完整。

这可能会在两种情况下导致问题:

    您正在尝试使用某些库或其他使用 async void 的库。抱歉,但事实是图书馆坏了,必须修复。 您将 EAP 组件包装到 Task 并正确使用 await。这可能会导致问题,因为 EAP 组件直接与 SynchronizationContext 交互。在这种情况下,最好的解决方案是修改类型,使其自然支持 TAP 或将其替换为 TAP 类型(例如,HttpClient 而不是 WebClient)。否则,您可以使用 TAP-over-APM 而不是 TAP-over-EAP。如果这些都不可行,您可以在 TAP-over-EAP 包装器周围使用 Task.Run

关于“一劳永逸”:

我个人从不将这个短语用于async void 方法。一方面,错误处理语义肯定不适合“一劳永逸”这句话;我半开玩笑地将async void 方法称为“火灾和崩溃”。真正的async“即发即弃”方法是async Task 方法,您可以忽略返回的Task,而不是等待它。

也就是说,在 ASP.NET 中,您几乎永远不想从请求中提前返回(这就是“一劳永逸”的含义)。这个答案已经太长了,但如果真的有必要,我有一个description of the problems on my blog, along with some code to support ASP.NET "fire and forget"。

【讨论】:

火灾和崩溃......很好。感谢您的详细回复。对我来说仍然缺少的一件是当我执行“var task = this.FireAndForgetTask()”时会发生什么。框架实际上对返回的任务做了什么?我不会将它存储在任何地方,也不会等待它。在这种情况下,是不是更接近于火而忘却?由于我们不在 EAP 领域,因此没有直接与 SynchronizationContext 交互的代码,并且不等待任务真的会忘记它吗? 如果你从来没有使用过task,那么就没有任何用处。该框架甚至不知道它的存在。是的,这是“一劳永逸”,但请注意我的博客文章中关于这样做的警告。 优秀。我想我现在明白了。感谢您的回答和其他有用的链接。 @DavidKreps - 我也有类似的情况。你能告诉我你做了什么来实现上述目标吗?谢谢 @Ditty - 我想我不明白你的问题。您希望对上面所写内容的哪一部分进行澄清?

以上是关于Async Void、ASP.Net 和未完成操作计数的主要内容,如果未能解决你的问题,请参考以下文章

asp.net webform中使用async,await实现异步操作

asp.net webform中使用async,await实现异步操作

为啥总是在 asp.net mvc 中同步异步操作(async await)

async / await 如何在 ASP.Net 应用程序中提供帮助?

在 asp.net 中,使用 Async 技术将占用多少工作线程

使 ASP.net MVC 5 Routing Ignore Async Suffix on Action 方法