Usar métodos asincrónicos en ASP.NET MVC 4

por Rick Anderson

Este tutorial le enseñará los conceptos básicos de la creación de una aplicación web ASP.NET MVC asincrónica mediante Visual Studio Express 2012 para Web, que es una versión gratuita de Microsoft Visual Studio. También puede usar Visual Studio 2012.

En github https://github.com/RickAndMSFT/Async-ASP.NET/ se proporciona un ejemplo completo para este tutorial

La clase Controller de ASP.NET MVC 4 junto con .NET 4.5 le permite escribir métodos de acción asincrónicos que devuelven un objeto de tipo Task<ActionResult>. En .NET Framework 4 se introdujo un concepto de programación asincrónico denominado Tarea y en ASP.NET MVC 4 se admite Tarea. Las tareas se representan mediante el tipo Task y los tipos relacionados en el espacio de nombres System.Threading.Tasks. .NET Framework 4.5 se basa en esta compatibilidad asincrónica con las palabras clave await y async que hacen que el trabajo con objetos Task sea mucho menos complejo que los enfoques asincrónicos anteriores. La palabra clave await es una abreviatura sintáctica para indicar que un fragmento de código debe esperar de forma asincrónica por otro fragmento de código. La palabra clave async representa una sugerencia que puede usar para marcar métodos como asincrónicos basados en tareas. La combinación de await, asyncy el objetoTask facilita mucho la escritura de código asincrónico en .NET 4.5. El nuevo modelo para métodos asincrónicos se denomina Patrón asincrónico basado en tareas (TAP). En este tutorial se supone que está familiarizado con la programación asincrónica mediante palabras clave await y async, y el espacio de nombres Task.

Para más información sobre el uso de las palabras clave await y async, y el espacio de nombres Task, vea las siguientes referencias.

Procesamiento de solicitudes en el grupo de subprocesos

En el servidor web, .NET Framework mantiene un grupo de subprocesos que se utilizan para dar servicio a las solicitudes de ASP.NET. Cuando se recibe una solicitud, se envía un subproceso del grupo para procesarla. Si la solicitud se procesa de manera sincrónica, el subproceso está ocupado mientras la procesa, de modo que no puede prestar servicio a otra solicitud.

Esto podría no ser un problema, porque se puede crear un grupo de subprocesos lo bastante grande para alojar numerosos subprocesos ocupados. Pero el número de subprocesos del grupo de subprocesos es limitado (el máximo predeterminado para .NET 4.5 es de 5000). En aplicaciones grandes con alta simultaneidad de solicitudes de larga duración, es posible que todos los subprocesos disponibles estén ocupados. Esta situación se denomina colapso de los subprocesos. Cuando se llega a esta situación, el servidor web pone las solicitudes en cola. Si la cola de solicitudes se llena, el servidor web rechaza las solicitudes y muestra el estado HTTP 503 (Servidor muy ocupado). El grupo de subprocesos de CLR tiene limitaciones en las inyecciones de subprocesos nuevos. Si la simultaneidad tiene ráfagas (es decir, el sitio web puede obtener repentinamente un gran número de solicitudes) y todos los subprocesos de solicitud disponibles están ocupados debido a las llamadas de back-end con una latencia alta, la tasa limitada de inyección de subprocesos puede hacer que la aplicación responda muy mal. Además, cada subproceso nuevo que se agrega al grupo de subprocesos tiene sobrecarga (por ejemplo, 1 MB de memoria de pila). Una aplicación web que use métodos sincrónicos para atender llamadas de alta latencia en las que el grupo de subprocesos crece hasta el máximo predeterminado de .NET 4.5 de 5000 subprocesos consumiría aproximadamente 5 GB más de memoria que una aplicación capaz de atender las mismas solicitudes mediante métodos asincrónicos y solo 50 subprocesos. Cuando realiza un trabajo asincrónico, no siempre usa un subproceso. Por ejemplo, al realizar una solicitud de servicio web asincrónica, ASP.NET no usará ningún subproceso entre la llamada al método async y await . El uso del grupo de subprocesos para atender las solicitudes con alta latencia puede provocar una gran superficie de memoria y un uso deficiente del hardware del servidor.

Procesamiento de solicitudes asincrónicas

En las aplicaciones web que ven un gran número de solicitudes simultáneas al inicio o tienen una carga con ráfagas (donde la simultaneidad aumenta repentinamente), realizar llamadas de servicio web asincrónicas aumenta la capacidad de respuesta de la aplicación. Una solicitud asincrónica tarda el mismo tiempo en procesarse que una sincrónica. Si una solicitud realiza una llamada de servicio web que necesita dos segundos para completarse, la solicitud tardará dos segundos con independencia de que se procese de manera sincrónica o asincrónica. Pero durante una llamada asincrónica, el subproceso no está ocupado para responder a otras solicitudes mientras espera que se complete la primera. Por tanto, las solicitudes asincrónicas impiden el crecimiento del grupo de subprocesos y las colas de solicitudes cuando hay muchas solicitudes simultáneas que invocan operaciones de larga duración.

Elección de métodos de acción sincrónicos o asincrónicos

En esta sección se muestran las instrucciones para decidir cuándo utilizar métodos de acción sincrónicos o asincrónicos. Se trata de meras instrucciones; debe estudiar individualmente cada aplicación para determinar si los métodos asincrónicos ayudan a mejorar el rendimiento.

En general, use métodos sincrónicos para las condiciones siguientes:

  • Las operaciones son simples o de ejecución breve.
  • La simplicidad es más importante que la eficacia.
  • Las operaciones son principalmente operaciones de la CPU y no operaciones que necesiten una gran sobrecarga del disco o de la red. El uso de métodos de acción asincrónicos en operaciones relacionadas con la CPU no proporciona ninguna ventaja y da lugar a mayor sobrecarga.

En general, use métodos asincrónicos para las condiciones siguientes:

  • Va a llamar a servicios que se pueden consumir mediante métodos asincrónicos y usa .NET 4.5 o posterior.
  • Las operaciones están relacionadas con la red o con E/S y no con la CPU.
  • El paralelismo es más importante que la simplicidad del código.
  • Quiere proporcionar un mecanismo que permita a los usuarios cancelar una solicitud de ejecución prolongada.
  • Cuando la ventaja de cambiar subprocesos supera el costo del modificador de contexto. En general, debe convertir un método en asincrónico si el método sincrónico mantiene ocupado el subproceso de solicitud de ASP.NET mientras no realiza ningún trabajo. Al realizar la llamada asincrónica, el subproceso de solicitud de ASP.NET no está ocupado sin trabajar mientras espera a que se complete la solicitud de servicio web.
  • Las pruebas muestran que las operaciones ocupadas constituyen un cuello de botella para el rendimiento del sitio y que IIS puede prestar servicio a más solicitudes si se utilizan métodos asincrónicos para estas llamadas que están ocupadas.

En el ejemplo descargable se muestra cómo utilizar con eficacia los métodos de acción asincrónicos. El ejemplo proporcionado se ha diseñado para proporcionar una demostración sencilla de la programación asincrónica en ASP.NET MVC 4 con .NET 4.5. El ejemplo no está pensado como una arquitectura de referencia para la programación asincrónica en ASP.NET MVC. El programa de ejemplo llama a ASP.NET Web API que, a su vez, llama a Task.Delay para simular llamadas de servicio web de larga duración. La mayoría de las aplicaciones de producción no mostrarán ventajas tan evidentes del uso de métodos asincrónicos.

Algunas aplicaciones exigen que todos los métodos de acción sean asincrónicos. Con frecuencia, basta con convertir algunos métodos de acción sincrónicos en métodos asincrónicos a fin de obtener la máxima eficacia para la cantidad de trabajo requerida.

Aplicación de ejemplo

Puede descargar la aplicación de ejemplo desde https://github.com/RickAndMSFT/Async-ASP.NET/ en el sitio de GitHub. El repositorio consta de tres proyectos:

  • Mvc4Async: el proyecto ASP.NET MVC 4 que contiene el código usado en este tutorial. Realiza llamadas API web al servicio WebAPIpgw.
  • WebAPIpgw: el proyecto de API web de ASP.NET MVC 4 que implementa los controladoresProducts, Gizmos and Widgets. Proporciona los datos para el proyecto WebAppAsync y el proyectoMvc4Async.
  • WebAppAsync: el proyecto de ASP.NET Web Forms que se usa en otro tutorial.

Método de acción sincrónico Gizmos

En el código siguiente se muestra el método de acción sincrónicoGizmos que se usa para mostrar una lista de gizmos. (Para este artículo, un gizmo es un dispositivo mecánico ficticio).

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}

En el código siguiente se muestra el métodoGetGizmos del servicio gizmo.

public class GizmoService
{
    public async Task<List<Gizmo>> GetGizmosAsync(
        // Implementation removed.
       
    public List<Gizmo> GetGizmos()
    {
        var uri = Util.getServiceUri("Gizmos");
        using (WebClient webClient = new WebClient())
        {
            return JsonConvert.DeserializeObject<List<Gizmo>>(
                webClient.DownloadString(uri)
            );
        }
    }
}

El métodoGizmoService GetGizmos pasa un URI a un servicio HTTP de API web de ASP.NET que devuelve una lista de datos de gizmos. El proyecto WebAPIpgw contiene la implementación de la API web gizmos, widget y los controladores product.
En la imagen siguiente se muestra la vista de gizmos del proyecto de ejemplo.

Gizmos

Creación de un método de acción Gizmos asincrónico

En el ejemplo se usan las nuevas palabras clave async y await (disponibles en .NET 4.5 y Visual Studio 2012) para permitir que el compilador sea responsable de mantener las transformaciones complicadas necesarias para la programación asincrónica. El compilador permite escribir código mediante las construcciones de flujo de control sincrónicas de C# y aplica automáticamente las transformaciones necesarias para usar devoluciones de llamada que evitan que los subprocesos estén ocupados.

En el código siguiente se muestra el método sincrónico Gizmos y el método asincrónico GizmosAsync. Si el explorador admite el elemento <mark> de HTML 5, verá los cambios en GizmosAsync resaltados en color amarillo.

public ActionResult Gizmos()
{
    ViewBag.SyncOrAsync = "Synchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", gizmoService.GetGizmos());
}
public async Task<ActionResult> GizmosAsync()
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos", await gizmoService.GetGizmosAsync());
}

Se han aplicado los siguientes cambios para permitir que GizmosAsync sea asincrónico.

  • El método se marca con la palabra clave async, que indica al compilador que genere devoluciones de llamada para partes del cuerpo y que cree automáticamente una instancia de Task<ActionResult> para devolver.
  • "Async" se ha anexado al nombre del método. No es necesario anexar "Async", pero es la convención al escribir métodos asincrónicos.
  • El tipo de valor devuelto se ha cambiado de ActionResult a Task<ActionResult>. El tipo de valor devuelto de Task<ActionResult> representa el trabajo en curso y proporciona a los autores de la llamada del método un manipulador con el que esperar a que finalice la operación asincrónica. En este caso, el autor de la llamada es el servicio web. Task<ActionResult> representa el trabajo continuo con un resultado de ActionResult.
  • La palabra clave await se ha aplicado a la llamada de servicio web.
  • Se ha llamado a la API de servicio web asincrónica (GetGizmosAsync).

Dentro del cuerpo del método GetGizmosAsync se llama a otro método asincrónico, GetGizmosAsync. GetGizmosAsync devuelve inmediatamente una instancia de Task<List<Gizmo>> que finalizará eventualmente cuando los datos estén disponibles. Como no quiere hacer nada más hasta que tenga los datos de gizmo, el código espera la tarea (mediante la palabra clave await). Solo puede usar la palabra clave await en los métodos anotados con la palabra clave async.

La palabra clave await no ocupa el subproceso hasta que se complete la tarea. Registra el resto del método como una devolución de llamada en la tarea y devuelve inmediatamente. Cuando la tarea en espera se complete, invocará esa devolución de llamada y, por tanto, reanudará la ejecución del método justo donde haya parado. Para más información sobre el uso de las palabras clave await y async, y el espacio de nombres Task, vea las referencias asincrónicas.

El siguiente código muestra los métodos GetGizmos y GetGizmosAsync.

public List<Gizmo> GetGizmos()
{
    var uri = Util.getServiceUri("Gizmos");
    using (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            webClient.DownloadString(uri)
        );
    }
}
public async Task<List<Gizmo>> GetGizmosAsync()
{
    var uri = Util.getServiceUri("Gizmos");
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

Los cambios asincrónicos son similares a los realizados anteriormente en GizmosAsync.

  • La firma del método se ha anotado con la palabra clave async, el tipo de valor devuelto se ha cambiado a Task<List<Gizmo>> y se ha anexado Async al nombre del método.
  • La clase HttpClient asincrónica se usa en lugar de la clase WebClient.
  • La palabra clave await se ha aplicado a los métodos asincrónicos HttpClient.

En la imagen siguiente se muestra la vista asincrónica de gizmo.

async

La presentación en los exploradores de los datos de gizmos es idéntica a la vista creada por la llamada sincrónica. La única diferencia es que la versión asincrónica puede ser más eficaz en cargas pesadas.

Realización de varias operaciones en paralelo

Los métodos de acción asincrónicos tienen una ventaja significativa sobre los métodos sincrónicos cuando una acción debe realizar varias operaciones independientes. En el ejemplo proporcionado, el método sincrónico PWG (para Productos, Widgets y Gizmos) muestra los resultados de tres llamadas de servicio web para obtener una lista de productos, widgets y gizmos. El proyecto deAPI web de ASP.NET que proporciona estos servicios usa Task.Delay para simular la latencia o llamadas de red lentas. Cuando el retraso se establece en 500 milisegundos, el método PWGasyncasincrónico tarda poco más de 500 milisegundos en completarse mientras que la versión PWG sincrónica tarda más de 1500 milisegundos. El método PWG sincrónico se muestra en el código siguiente.

public ActionResult PWG()
{
    ViewBag.SyncType = "Synchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );

    return View("PWG", pwgVM);
}

El método PWGasync asincrónico se muestra en el código siguiente.

public async Task<ActionResult> PWGasync()
{
    ViewBag.SyncType = "Asynchronous";
    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var widgetTask = widgetService.GetWidgetsAsync();
    var prodTask = prodService.GetProductsAsync();
    var gizmoTask = gizmoService.GetGizmosAsync();

    await Task.WhenAll(widgetTask, prodTask, gizmoTask);

    var pwgVM = new ProdGizWidgetVM(
       widgetTask.Result,
       prodTask.Result,
       gizmoTask.Result
       );

    return View("PWG", pwgVM);
}

En la imagen siguiente se muestra la vista devuelta desde el método PWGasync.

pwgAsync

Uso de un token de cancelación

Los métodos de acción asincrónicos que devuelven Task<ActionResult> son cancelables, es decir, toman un parámetro CancellationToken cuando se proporciona uno con el atributo AsyncTimeout. En el código siguiente se muestra el método GizmosCancelAsync con un tiempo de espera de 150 milisegundos.

[AsyncTimeout(150)]
[HandleError(ExceptionType = typeof(TimeoutException),
                                    View = "TimeoutError")]
public async Task<ActionResult> GizmosCancelAsync(
                       CancellationToken cancellationToken )
{
    ViewBag.SyncOrAsync = "Asynchronous";
    var gizmoService = new GizmoService();
    return View("Gizmos",
        await gizmoService.GetGizmosAsync(cancellationToken));
}

En el código siguiente se muestra la sobrecarga GetGizmosAsync, que toma un parámetro CancellationToken.

public async Task<List<Gizmo>> GetGizmosAsync(string uri,
    CancellationToken cancelToken = default(CancellationToken))
{
    using (HttpClient httpClient = new HttpClient())
    {
        var response = await httpClient.GetAsync(uri, cancelToken);
        return (await response.Content.ReadAsAsync<List<Gizmo>>());
    }
}

En la aplicación de ejemplo proporcionada, al seleccionar el vínculo Demostración del token de cancelación se llama al método GizmosCancelAsync y se muestra la cancelación de la llamada asincrónica.

Configuración del servidor para llamadas de servicio web de alta simultaneidad o latencia

Para apreciar los beneficios de una aplicación web asincrónica, es posible que tenga que realizar algunos cambios en la configuración predeterminada del servidor. Tenga en cuenta lo siguiente al configurar y realizar la prueba de esfuerzo de la aplicación web asincrónica.