54

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

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

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

从这些资源中,我了解到最佳实践通常是返回 Task 并避免异步无效。我也明白 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 实际上并没有忘记它?

4

1 回答 1

73

asyncMicrosoft 决定在引入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.NETSynchronizationContext得到了增强,但出现了您所看到的异常;它会发现许多使用错误。

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

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

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

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

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

关于“一劳永逸”:

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

也就是说,在 ASP.NET 中,您几乎永远不想从请求中提前返回(这就是“一劳永逸”的含义)。这个答案已经太长了,但我在我的博客上有对问题的描述,以及一些支持 ASP.NET “即发即忘”的代码,如果确实有必要的话。

于 2013-07-15T17:44:00.530 回答