Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async Void, ASP.Net, and Count of Outstanding Operations

I am trying to understand why an async void method in an ASP.Net application can result in the following exception, while it appears that async Task will not:

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

I am relatively new to the world of async in .NET, but do feel like I've tried to run this one down via a number of existing resources, including all of the following:

  • What's the difference between returning void and returning a Task?
  • It's All About the SynchronizationContext
  • Async Syntactic Sugar Suggestions
  • Async in ASP.NET

From these resources, I understand the best practice is to typically return Task and avoid async void. I also understand that async void increments the count of outstanding operations when the method is called and decrements it when it is completed. This sounds like at least part of the answer to my question. However, what I am missing is what happens when I return Task and why doing so makes things "work".

Here is a contrived example to further illustrate my question:

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;     } } 

On a related note, if my understanding of async void is correct, then isn't it kind of wrong to think of async void as "fire and forget" in this scenario since ASP.Net is not actually forgetting about it?

like image 661
David Kreps Avatar asked Jul 15 '13 16:07

David Kreps


People also ask

Why you shouldn't use async void?

Async void methods can wreak havoc if the caller isn't expecting them to be async. When the return type is Task, the caller knows it's dealing with a future operation; when the return type is void, the caller might assume the method is complete by the time it returns.

What is the difference between async void and async task?

A Task returning async method can be awaited, and when the task completes, the continuation of the task is scheduled to run. A void returning async method cannot be awaited; it is a "fire and forget" method. It does work asynchronously, and you have no way of telling when it is done.

Can async return void?

In short, if your async method is an event handler or a callback, it's ok to return void .

How does async void work?

async void has the same semantics as async Task , except for exceptions. An async void method will capture the current SynchronizationContext at the beginning of the method, and any exceptions from that method will be captured and raised directly on that captured context.


1 Answers

Microsoft made the decision to avoid as much backwards-compatibility issues as possible when bringing async into ASP.NET. And they wanted to bring it to all of their "one ASP.NET" - so async support for WinForms, MVC, WebAPI, SignalR, etc.

Historically, ASP.NET has supported clean asynchronous operations since .NET 2.0 via the Event-based Asynchronous Pattern (EAP), in which asynchronous components notify the SynchronizationContext of their starting and completing. .NET 4.5 brings the first fairly hefty changes to this support, updating the core ASP.NET asynchronous types to better enable the Task-based Asynchronous Pattern (TAP, i.e., async).

In the meantime, each different framework (WebForms, MVC, etc) all developed their own way to interact with that core, keeping backwards compatibility a priority. In an attempt to assist developers, the core ASP.NET SynchronizationContext was enhanced with the exception you're seeing; it will catch many usage mistakes.

In the WebForms world, they have RegisterAsyncTask but a lot of people just use async void event handlers instead. So the ASP.NET SynchronizationContext will allow async void at appropriate times during the page lifecycle, and if you use it at an inappropriate time it will raise that exception.

In the MVC/WebAPI/SignalR world, the frameworks are more structured as services. So they were able to adopt async Task in a very natural fashion, and the framework only has to deal with the returned Task - a very clean abstraction. As a side note, you don't need AsyncController anymore; MVC knows it's asynchronous just because it returns a Task.

However, if you try to return a Task and use async void, that's not supported. And there's little reason to support it; it would be quite complex just to support users that aren't supposed to be doing that anyway. Remember that async void notifies the core ASP.NET SynchronizationContext directly, bypassing the MVC framework completely. The MVC framework understands how to wait for your Task but it doesn't even know about the async void, so it returns completion to the ASP.NET core which sees that it's not actually complete.

This can cause problems in two scenarios:

  1. You're trying to use some library or whatnot that uses async void. Sorry, but the plain fact is that the library is broken, and will have to be fixed.
  2. You're wrapping an EAP component into a Task and properly using await. This can cause problems because the EAP component interacts with SynchronizationContext directly. In this case, the best solution is to modify the type so it supports TAP naturally or replace it with a TAP type (e.g., HttpClient instead of WebClient). Failing that, you can use TAP-over-APM instead of TAP-over-EAP. If neither of those are feasible, you can just use Task.Run around your TAP-over-EAP wrapper.

Regarding "fire and forget":

I personally never use this phrase for async void methods. For one thing, the error handling semantics most certainly do not fit in with the phrase "fire and forget"; I half-jokingly refer to async void methods as "fire and crash". A true async "fire and forget" method would be an async Task method where you ignore the returned Task rather than waiting for it.

That said, in ASP.NET you almost never want to return early from requests (which is what "fire and forget" implies). This answer is already too long, but I have a description of the problems on my blog, along with some code to support ASP.NET "fire and forget" if it's truly necessary.

like image 109
Stephen Cleary Avatar answered Oct 04 '22 16:10

Stephen Cleary