我试图理解为什么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实际上并没有忘记它?
最佳答案
Microsoft决定将async
引入ASP.NET时,尽可能避免向后兼容的问题。他们希望将其引入所有“一个ASP.NET”中-因此对WinForms,MVC,WebAPI,SignalR等的async
支持。
从历史上看,ASP.NET从.NET 2.0开始就通过基于事件的异步模式(EAP)支持干净的异步操作,其中异步组件将其开始和完成通知SynchronizationContext
。 .NET 4.5对该支持进行了首次重大更改,更新了核心ASP.NET异步类型以更好地启用基于任务的异步模式(TAP,即async
)。
同时,每个不同的框架(WebForm,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
的库。抱歉,但显而易见的事实是该库已损坏,必须将其修复。 Task
中,并正确使用await
。这可能会导致问题,因为EAP组件直接与SynchronizationContext
交互。在这种情况下,最好的解决方案是修改类型,使其自然支持TAP或将其替换为TAP类型(例如HttpClient
而不是WebClient
)。失败的话,您可以使用TAP上的TAP而不是EAP上的TAP。如果这两种方法都不可行,则可以在TAP-over-EAP包装器周围使用Task.Run
。 关于“忘了忘了”:
我个人从未将此短语用于
async void
方法。一方面,错误处理语义最肯定不适合短语“即发即弃”。我 Jest 将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"。