异步 Void、ASP.Net 和未完成操作数量

57

我正在尝试理解为什么在ASP.Net应用程序中的异步void方法可能会导致以下异常,而异步任务似乎不会:

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

我对.NET中的异步世界相对较新,但我感觉已经通过多种现有资源(包括以下所有内容)尝试了解这个问题: 从这些资源中,我了解到最佳实践通常是返回Task并避免使用async 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当作“fire and forget”是不是有点错误,因为ASP.Net实际上并没有忘记它呢?


1
“async void” 是一种“fire and forget”的方式,因为您无法使用“await”等待它,因为它不返回可等待的“Task”。 - Daniel Mann
1个回答

75

在将async引入ASP.NET时,Microsoft决定尽可能避免向后不兼容的问题。他们希望将其引入到其“一个ASP.NET”中的所有内容 - 因此对WinForms、MVC、WebAPI、SignalR等进行async支持。

从历史上看,自.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 核心,而 ASP.NET 核心会发现实际上并没有完成。

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

  1. 你正在尝试使用一些使用 async void 的库或其他内容。很抱歉,事实上这个库是有问题的,必须进行修复。
  2. 你正在将 EAP 组件包装到一个 Task 中,并正确使用 await。这可能会引起问题,因为 EAP 组件直接与 SynchronizationContext 交互。在这种情况下,最好的解决方案是修改该类型,使其自然地支持 TAP 或用 TAP 类型(例如 HttpClient 而不是 WebClient)替换它。如果这些都不可行,您可以在 TAP-over-EAP 包装器周围使用 Task.Run

关于 "fire and forget":

我个人从不将此短语用于 async void 方法。首先,错误处理的语义显然与 "fire and forget" 的短语不符;我半开玩笑地将 async void 方法称为 "fire and crash"。一个真正的 async 的 "fire and forget" 方法将是一个 async Task 方法,在该方法中忽略返回的 Task 而不是等待它。

话虽如此,在ASP.NET中,您几乎从不希望提前结束请求(这就是“点火并忘记”所意味的)。本答案已经太长了,但是如果确实必要,我在我的博客上有一些代码来支持ASP.NET的“点火并忘记”,并附有相关问题的描述。


火灾和崩溃...很好。感谢您详细的回复。对我来说仍然缺少的一部分是当我执行“var task = this.FireAndForgetTask()”时会发生什么。框架实际上如何处理返回的任务?我既没有将其存储在任何地方,也没有等待它。在这种情况下,它更接近于“fire and forget”吗?由于我们不处于EAP领域,因此没有与同步上下文直接交互的代码,并且不等待任务真正忘记了它? - David Kreps
1
如果您从未使用task,那么没有任何东西会使用它。框架甚至不知道它的存在。是的,这是“fire and forget”,但请注意我的博客文章中关于这样做的警告。 - Stephen Cleary
太好了,我现在明白了。感谢你的回答和额外的有用链接。 - David Kreps
@DavidKreps - 我也遇到了类似的情况。你能告诉我你是如何实现上述功能的吗?谢谢。 - Ditty
@Ditty - 我不认为我理解你的问题。 你需要澄清以上哪个部分呢? - David Kreps
我遇到了这个错误,但是我的代码中没有异步 void。 - Brian

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接