Async Void, ASP.Net y recuento de operaciones pendientes

Estoy tratando de entender por qué un método de vacío asíncrono en una aplicación ASP.Net puede dar como resultado la siguiente excepción, mientras que parece que la tarea asíncrona no:

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

Soy relativamente nuevo en el mundo de la asincronización en .NET, pero siento que he tratado de ejecutar esto a través de una serie de recursos existentes, que incluyen todo lo siguiente:

  • ¿Cuál es la diferencia entre devolver el vacío y devolver una Tarea?
  • Se trata del SynchronizationContext
  • Sugerencias asincrónicas de azúcar sintáctica
  • Async en ASP.NET

A partir de estos recursos, entiendo que la mejor práctica es típicamente devolver Tarea y evitar el vacío asíncrono. También entiendo que async void incrementa el recuento de operaciones pendientes cuando se llama al método y lo disminuye cuando se completa. Esto parece al menos parte de la respuesta a mi pregunta. Sin embargo, lo que me falta es lo que sucede cuando devuelvo Tarea y por qué hacerlo “funciona”.

Aquí hay un ejemplo artificial para ilustrar más mi pregunta:

 public class HomeController : AsyncController { // This method will work fine public async Task 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 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; } } 

En una nota relacionada, si mi comprensión del vacío asíncrono es correcta, entonces ¿no está mal pensar en un vacío asincrónico como “disparar y olvidar” en este escenario ya que ASP.Net en realidad no se está olvidando de eso?

Microsoft tomó la decisión de evitar la mayor cantidad de problemas de compatibilidad con versiones anteriores cuando incorporase la función async en ASP.NET. Y querían llevarlo a todos sus “un ASP.NET”, de modo que async soporte para WinForms, MVC, WebAPI, SignalR, etc.

Históricamente, ASP.NET ha admitido operaciones asincrónicas limpias desde .NET 2.0 a través del Patrón asincrónico basado en eventos (EAP), en el que los componentes asíncronos notifican al SynchronizationContext su inicio y finalización. .NET 4.5 trae los primeros cambios bastante importantes a este soporte, actualizando los tipos asincrónicos principales de ASP.NET para habilitar mejor el Patrón asincrónico basado en tareas (TAP, es decir, async ).

Mientras tanto, cada marco diferente (WebForms, MVC, etc.) desarrolló su propia forma de interactuar con ese núcleo, manteniendo la compatibilidad con versiones anteriores como una prioridad. En un bash de ayudar a los desarrolladores, el núcleo de ASP.NET SynchronizationContext fue mejorado con la excepción que está viendo; atrapará muchos errores de uso.

En el mundo de WebForms, tienen RegisterAsyncTask pero mucha gente simplemente usa manejadores de eventos async void . Por lo tanto, ASP.NET SynchronizationContext permitirá la async void SynchronizationContext en los momentos apropiados durante el ciclo de vida de la página, y si la usa en un momento inadecuado, generará esa excepción.

En el mundo MVC / WebAPI / SignalR, los marcos están más estructurados como servicios. Así que pudieron adoptar la async Task de una manera muy natural, y el marco solo tiene que lidiar con la Task devuelta, una abstracción muy limpia. Como nota al margen, ya no necesitas AsyncController ; MVC sabe que es asincrónico solo porque devuelve una Task .

Sin embargo, si intentas devolver una Task y usar un async void , eso no es compatible. Y hay pocas razones para apoyarlo; Sería bastante complejo solo apoyar a los usuarios que se supone que no deben hacer eso de todos modos. Recuerde que async void notifica al núcleo ASP.NET SynchronizationContext directamente, omitiendo completamente el framework MVC. El marco de MVC entiende cómo esperar su Task pero ni siquiera conoce el async void , por lo que devuelve la finalización al núcleo de ASP.NET, que ve que en realidad no está completo.

Esto puede causar problemas en dos escenarios:

  1. Estás tratando de usar alguna biblioteca o lo que sea que use el async void . Lo siento, pero el hecho es que la biblioteca está rota y tendrá que ser reparada.
  2. Está envolviendo un componente EAP en una Task y utilizando adecuadamente await . Esto puede causar problemas porque el componente EAP interactúa directamente con SynchronizationContext . En este caso, la mejor solución es modificar el tipo para que sea compatible con TAP de forma natural o sustituirlo por un tipo de TAP (por ejemplo, HttpClient lugar de WebClient ). De lo contrario, puede usar TAP-over-APM en lugar de TAP-over-EAP. Si ninguno de estos es factible, puede usar Task.Run alrededor de su Task.Run TAP-over-EAP.

En cuanto a “disparar y olvidar”:

Personalmente, nunca uso esta frase para los métodos de async void . Por un lado, la semántica de manejo de errores ciertamente no encaja con la frase “disparar y olvidar”; Medio en broma me refiero a los métodos de async void como “fuego y locking”. Un verdadero método async “disparar y olvidar” sería un método de async Task en el que ignoraría la Task devuelta en lugar de esperarla.

Dicho esto, en ASP.NET casi nunca desea regresar temprano de las solicitudes (que es lo que implica “disparar y olvidar”). Esta respuesta ya es demasiado larga, pero tengo una descripción de los problemas en mi blog, junto con algún código para apoyar ASP.NET “disparar y olvidar” si es realmente necesario.