小编典典

异步无效,ASP.Net和未完成操作的数量

c#

我试图理解为什么ASP.Net应用程序中的异步void方法会导致以下异常,而异步任务却不会:

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

我对.NET中的异步世界还比较陌生,但是确实感觉我已经尝试通过许多现有资源(包括以下所有资源)来简化此工作:

从这些资源中,我了解最佳实践是通常返回Task并避免异步void。我也了解,异步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;
    }
}

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


阅读 286

收藏
2020-05-19

共1个答案

小编典典

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

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

同时,每个不同的框架(WebForm,MVC等)都开发了自己的与该内核进行交互的方式,将 向后兼容性
作为优先事项。为了帮助开发人员,SynchronizationContext除了您看到的异常之外,还增强了核心ASP.NET 。它将捕获许多使用错误。

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

在MVC / WebAPI / SignalR世界中,框架被结构化为服务。因此,他们能够以async Task非常自然的方式采用,并且该框架只需要处理返回的内容Task-一个非常干净的抽象。附带说明,您不再需要AsyncController了。MVC知道它是异步的,因为它返回Task

但是,如果您尝试返回a Task 使用async void,则不支持。而且没有什么理由支持它。仅支持不应该这样做的用户将是非常复杂的。请记住,它async voidSynchronizationContext直接绕过MVC框架直接通知核心ASP.NET
。MVC框架了解如何等待您,Task但它甚至不了解async void,因此它将完成返回到ASP.NET核心,该核心看到它 实际上 尚未完成。

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

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

关于“忘了忘了”:

我个人从不使用此短语作为async void方法。一方面,错误处理的语义最肯定不适合短语“即发即弃”。我开玩笑地将async void方法称为“火灾和崩溃”。真正的async“解雇”方法将是async Task您忽略返回Task而不是等待返回的方法。

就是说,在ASP.NET中,您几乎永远都不想从请求中早返回(这就是“一劳永逸”的含义)。这个答案已经太长了,但是我确实在博客上描述了这些问题,并在确实需要的情况下提供了一些支持ASP.NET“即弃即用”的代码

2020-05-19