Usar métodos asincrónicos en ASP.NET 4.5

Por Rick Anderson

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

Se proporciona un ejemplo completo para este tutorial en
https://github.com/RickAndMSFT/Async-ASP.NET/ en el sitio de GitHub.

ASP.NET 4.5 Web Pages en combinación con .NET 4.5 permite registrar métodos asincrónicos que devuelven un objeto de tipo Tarea. .NET Framework 4 introdujo un concepto de programación asincrónico denominado Tarea y ASP.NET 4.5 admite Tarea. Las tareas se representan mediante el tipo de Tarea y los tipos relacionados en el espacio de nombres System.Threading.Tasks. .NET Framework 4.5 crea sobre esta compatibilidad asincrónica con las palabras clave await y async que hacen que el trabajo con objetos Tarea sea mucho menos complejo que los enfoques asincrónicos anteriores. La palabra clave await es abreviatura sintáctica para indicar que un fragmento de código debe esperar de forma asincrónica en algún 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 objetoTarea 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 (PAT). 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 Tarea.

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

Cómo se procesan las 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 ese subproceso no puede prestar servicio a otra solicitud.

Esto podría no dar lugar a ningún problema, porque se puede crear un grupo de subprocesos lo bastante grande para alojar numerosos subprocesos ocupados. Sin embargo, el número de subprocesos del grupo de subprocesos es limitado (el máximo predeterminado para .NET 4.5 es de 5 000). 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 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 backend con una latencia alta, la tasa limitada de inyección de subprocesos puede hacer que la aplicación responda muy mal. Además, cada nuevo subproceso agregado al grupo de subprocesos tiene sobrecarga (por ejemplo, 1 MB de memoria de pila). Una aplicación web que usa 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 5 000 subprocesos lo que consumiría aproximadamente 5 GB más 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.

Procesar solicitudes asincrónicas

En las aplicaciones web que ven un gran número de solicitudes simultáneas al inicio o tienen una carga de ráfaga (donde la simultaneidad aumenta repentinamente), realizar llamadas de servicio web asincrónicas aumentará la capacidad de respuesta de la aplicación. Una solicitud asincrónica tarda el mismo tiempo en procesarse que una sincrónica. Por ejemplo, si una solicitud realiza una llamada de servicio web que tarda dos segundos en completarse, la solicitud tardará dos segundos con independencia de que se procese de manera sincrónica o asincrónica. Sin embargo, durante una llamada asincrónica, el subproceso no está ocupado para responder a otras solicitudes mientras espera que se complete la primera. Por lo 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.

Elegir métodos sincrónicos o asincrónicos

En esta sección se muestran las directrices para decidir cuándo utilizar métodos sincrónicos o asincrónicos. Se trata de meras directrices; 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 requieren una gran sobrecarga del disco o de la red. El uso de métodos 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:

  • Está llamando a servicios que se pueden consumir a través de 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 de código.

  • Desea 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 realizar un método asincrónico si el método sincrónico mantiene ocupado el subproceso de solicitud 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 asincrónicos. El ejemplo proporcionado se diseñó para proporcionar una demostración sencilla de la programación asincrónica en ASP.NET 4.5. El ejemplo no está pensado para ser una arquitectura de referencia para la programación asincrónica en ASP.NET. 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. Pocas aplicaciones de producción muestran ventajas tan evidentes del uso de métodos asincrónicos.

Pocas aplicaciones exigen que todos los métodos sean asincrónicos. Con frecuencia, basta con convertir algunos métodos sincrónicos en métodos asincrónicos para 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:

  • WebAppAsync: el proyecto de ASP.NET Web Forms que consume el servicio WebAPIpwg de la API web. La mayoría del código de este tutorial procede de este proyecto.
  • WebAPIpgw: el proyecto de API web de ASP.NET MVC 4 que implementa los controladoresProducts, Gizmos and Widgets. Proporciona los datos del proyecto WebAppAsync y del proyectoMvc4Async.
  • Mvc4Async: el proyecto ASP.NET MVC 4 que contiene el código usado en otro tutorial. Realiza llamadas API web al servicio WebAPIpwg.

Página sincrónica de Gizmos

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

public partial class Gizmos : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var gizmoService = new GizmoService();
        GizmoGridView.DataSource = gizmoService.GetGizmos();
        GizmoGridView.DataBind();
    }
}

El código siguiente 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 webgizmos, widget y los controladoresproduct.
En la imagen siguiente se muestra la página gizmos del proyecto de ejemplo.

Screenshot of the Sync Gizmos web browser page showing the the table of gizmos with corresponding details as entered into the web API controllers.

Crear una página de Gizmos asincrónica

El ejemplo usa 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 el compilador aplica automáticamente las transformaciones necesarias para usar devoluciones de llamada que evitan que los subprocesos estén ocupados.

Las páginas asincrónicas de ASP.NET deben incluir la directiva Page con el atributo Async establecido en "true". El código siguiente muestra la directiva Page con el atributoAsync establecido en "true" para la página de GizmosAsync.aspx.

<%@ Page Async="true"  Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosAsync.aspx.cs" Inherits="WebAppAsync.GizmosAsync" %>

El código siguiente muestra el método Gizmos sincrónico Page_Load y la página asincrónicaGizmosAsync. Si el explorador admite HTML 5< para marcar > elementos, verá los cambios en resaltado amarilloGizmosAsync.

protected void Page_Load(object sender, EventArgs e)
{
   var gizmoService = new GizmoService();
   GizmoGridView.DataSource = gizmoService.GetGizmos();
   GizmoGridView.DataBind();
}

Versión asincrónica:

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcAsync));
}

private async Task GetGizmosSvcAsync()
{
    var gizmoService = new GizmoService();
    GizmosGridView.DataSource = await gizmoService.GetGizmosAsync();
    GizmosGridView.DataBind();
}

Se aplicaron los siguientes cambios para permitir que la páginaGizmosAsync sea asincrónica.

  • La directiva Page debe tener el atributo Async establecido en "true".
  • El métodoRegisterAsyncTask se usa para registrar una tarea asincrónica que contiene el código que se ejecuta de forma asincrónica.
  • El nuevo métodoGetGizmosSvcAsync 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 un Task que se devuelve.
  • "Async" se anexó al nombre del método asincrónico. No es necesario anexar "Async", pero es la convención al escribir métodos asincrónicos.
  • El tipo de valor devuelto del nuevo métodoGetGizmosSvcAsync es Task. El tipo de valor devuelto de Task representa el trabajo en curso y proporciona a los llamadores del método un manipulador a través del cual esperar a que finalice la operación asincrónica.
  • La palabra clave await se aplicó a la llamada de servicio web.
  • Se llamó a la API de servicio web asincrónica (GetGizmosAsync).

Dentro del cuerpo del método GetGizmosSvcAsync se llama a otro método asincrónico GetGizmosAsync. GetGizmosAsync devuelve inmediatamente un Task<List<Gizmo>> que finalizará eventualmente cuando los datos estén disponibles. Dado que no desea hacer nada más hasta que tenga los datos de gizmo, el código espera la tarea (mediante la palabra clave await). Puede usar la palabra clave await solo 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 devolución de llamada en la tarea y devuelve inmediatamente. Cuando la tarea esperada esté completa, invocará esa devolución de llamada y, por tanto, reanudará la ejecución del método justo donde se dejó. Para obtener más información sobre el uso de las palabras clave await y async y el espacio de nombres Tarea, 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 (WebClient webClient = new WebClient())
    {
        return JsonConvert.DeserializeObject<List<Gizmo>>(
            await webClient.DownloadStringTaskAsync(uri)
        );
    }
}

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

  • La firma del método se anotó con la palabra clave async, el tipo de valor devuelto se cambió a Task<List<Gizmo>> y Async se anexó al nombre del método.
  • La clase HttpClient asincrónica se usa en lugar de la clase WebClient sincrónica.
  • La palabra clave await se aplicó al método asincrónico HttpClientGetAsync.

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

Screenshot of the Gizmos Async web browser page showing the table of gizmos with corresponding details as entered into the web API controllers.

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.

Notas de RegisterAsyncTask

Los métodos conectados con RegisterAsyncTask se ejecutarán inmediatamente después de PreRender.

Si usa eventos de página async void directamente, como se muestra en el código siguiente:

protected async void Page_Load(object sender, EventArgs e) {
    await ...;
    // do work
}

ya no tiene control total sobre cuándo se ejecutan los eventos. Por ejemplo, si un .aspx y un .Master definen eventos Page_Load y uno o ambos son asincrónicos, no se puede garantizar el orden de ejecución. Se aplica el mismo orden indeterminado para los controladores de eventos (como async void Button_Click).

Realizar varias operaciones en paralelo

Los métodos 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, la página sincrónica PWG.aspx(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 de API 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, la página asincrónica PWGasync.aspx tarda un poco más de 500 milisegundos en completarse mientras que la versión sincrónica PWG toma más de 1500 milisegundos. La página sincrónica PWG.aspx se muestra en el código siguiente.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();

    var widgetService = new WidgetService();
    var prodService = new ProductService();
    var gizmoService = new GizmoService();

    var pwgVM = new ProdGizWidgetVM(
        widgetService.GetWidgets(),
        prodService.GetProducts(),
        gizmoService.GetGizmos()
       );
    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();

    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}", 
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

A continuación se muestra el código asincrónico PWGasyncsubyacente.

protected void Page_Load(object sender, EventArgs e)
{
    Stopwatch stopWatch = new Stopwatch();
    stopWatch.Start();
    RegisterAsyncTask(new PageAsyncTask(GetPWGsrvAsync));
    stopWatch.Stop();
    ElapsedTimeLabel.Text = String.Format("Elapsed time: {0}",
        stopWatch.Elapsed.Milliseconds / 1000.0);
}

private async Task GetPWGsrvAsync()
{
    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
       );

    WidgetGridView.DataSource = pwgVM.widgetList;
    WidgetGridView.DataBind();
    ProductGridView.DataSource = pwgVM.prodList;
    ProductGridView.DataBind();
    GizmoGridView.DataSource = pwgVM.gizmoList;
    GizmoGridView.DataBind();           
}

En la imagen siguiente se muestra la vista devuelta desde la página asincrónica PWGasync.aspx.

Screenshot of the Asynchronous Widgets, Products, and Gizmos web browser page showing the Widgets, Products, and Gizmos tables.

Usar un token de cancelación

Los métodos asincrónicos que devuelven Task son cancelables, es decir, toman un parámetro CancellationToken cuando se proporciona uno con el atributo AsyncTimeout de la directiva Page. En el código siguiente se muestra la página GizmosCancelAsync.aspx con un tiempo de espera de un segundo.

<%@ Page  Async="true"  AsyncTimeout="1" 
    Language="C#" AutoEventWireup="true" 
    CodeBehind="GizmosCancelAsync.aspx.cs" 
    Inherits="WebAppAsync.GizmosCancelAsync" %>

El código siguiente muestra el archivo GizmosCancelAsync.aspx.cs.

protected void Page_Load(object sender, EventArgs e)
{
    RegisterAsyncTask(new PageAsyncTask(GetGizmosSvcCancelAsync));
}

private async Task GetGizmosSvcCancelAsync(CancellationToken cancellationToken)
{
    var gizmoService = new GizmoService();
    var gizmoList = await gizmoService.GetGizmosAsync(cancellationToken);
    GizmosGridView.DataSource = gizmoList;
    GizmosGridView.DataBind();
}
private void Page_Error(object sender, EventArgs e)
{
    Exception exc = Server.GetLastError();

    if (exc is TimeoutException)
    {
        // Pass the error on to the Timeout Error page
        Server.Transfer("TimeoutErrorPage.aspx", true);
    }
}

En la aplicación de ejemplo proporcionada, al seleccionar el vínculo GizmosCancelAsync se llama a la página GizmosCancelAsync.aspx y se muestra la cancelación (por tiempo de espera) de la llamada asincrónica. Dado que el tiempo de retraso está dentro de un intervalo aleatorio, es posible que tenga que actualizar la página un par de veces para obtener el mensaje de error de tiempo de espera agotado.

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.

  • Windows 7, Windows Vista, Window 8 y todos los sistemas operativos cliente de Windows tienen un máximo de 10 solicitudes simultáneas. Necesitará un sistema operativo Windows Server para ver los beneficios de los métodos asincrónicos con una carga alta.

  • Registre .NET 4.5 con IIS desde un símbolo del sistema con privilegios elevados mediante el siguiente comando:
    %windir%\Microsoft.NET\Framework64 \v4.0.30319\aspnet_regiis -i
    Ver Herramienta de registro de IIS en ASP.NET (Aspnet_regiis.exe)

  • Es posible que tenga que aumentar el límite de cola de HTTP.sys del valor predeterminado de 1 000 a 5 000. Si la configuración es demasiado baja, es posible que vea a HTTP.sys rechazar solicitudes con un estado HTTP 503. Para cambiar el límite de cola de HTTP.sys:

    • Abra el administrador de IIS y vaya al panel grupos de aplicaciones.
    • Haga clic con el botón derecho en el grupo de aplicaciones de destino y seleccione Configuración avanzada.
      Screenshot of the Internet Information Services Manager showing the Advanced Settings menu highlighted with a red rectangle.
    • En el cuadro de diálogo Configuración avanzada, cambie Longitud de cola de 1 000 a 5 000.
      Screenshot of the Advanced Settings dialog box showing the Queue Length field set to 1000 and highlighted with a red rectangle.

    Tenga en cuenta en las imágenes anteriores, .NET Framework aparece como v4.0, aunque el grupo de aplicaciones use .NET 4.5. Para comprender esta discrepancia, consulte lo siguiente:

  • Control de versiones y compatibilidad con múltiples versiones de .NET: .NET 4.5 es una actualización local para .NET 4.0

  • Cómo establecer una aplicación de IIS o AppPool para usar ASP.NET 3.5 en lugar de 2.0

  • Versiones y dependencias de .NET Framework

  • Si la aplicación usa servicios web o System.NET para comunicarse con un back-end a través de HTTP, es posible que tenga que aumentar el elemento connectionManagement/maxconnection. En el caso de las aplicaciones ASP.NET, esta característica está limitada por la característica autoConfig a 12 veces la cantidad de CPU. Esto significa que, en un procesador cuádruple, puede tener como máximo 12 * 4 = 48 conexiones simultáneas a un punto de conexión IP. Dado que esto está vinculado a autoConfig, la manera más fácil de aumentarmaxconnection en una aplicación de ASP.NET es establecer System.Net.ServicePointManager.DefaultConnectionLimitmediante programación en la forma del método Application_Start del archivo global.asax. Consulte la descarga de muestra para obtener un ejemplo.

  • En .NET 4.5, el valor predeterminado de 5 000 para MaxConcurrentRequestsPerCPU debería ser correcto.

Colaboradores