Async Void,ASP.Net和杰出运营计数

时间:2013-07-15 16:54:49

标签: c# asp.net async-await

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

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

我对.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实际上并没有忘记它吗?

1 个答案:

答案 0 :(得分:57)

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

从历史上看,ASP.NET通过基于事件的异步模式(EAP)支持自.NET 2.0以来的干净异步操作,其中异步组件通知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核心,它看到它实际上不是 完成。

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

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

  3. 关于“火与忘记”:

    我个人从未在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"