Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 11 de septiembre de 2012
ASP.NET MVCEste es el mensaje que deberíamos interiorizar, si no lo hemos hecho ya, a la vista de las múltiples novedades introducidas en las últimas versiones de la plataforma ASP.NET y MVC:

Asíncronía = bueno

A grandes rasgos, la explicación es la siguiente: IIS tiene disponible un número limitado de hilos (threads) destinados a procesar las peticiones. Cuando llega una petición, uno de estos hilos es asignado en exclusiva a ella y permanecerá ocupado hasta que haya sido totalmente procesada. Si llegan peticiones cuando todos los hilos están ocupados, se introducen en una cola, también limitada. Cuando el tamaño máximo de esta cola ha sido superado, ya al servidor no le queda más remedio que responder con un error HTTP 503 al usuario indicándole que está seriamente ocupado.

Si las peticiones son “despachadas” de forma rápida es realmente difícil alcanzar estos límites. El problema aparece cuando el proceso de las peticiones requieren la ejecución de alguna tarea de larga duración que dependa de recursos externos, como puede ser una consulta pesada a la base de datos o la obtención de información desde un servicio web; en estos casos, el hilo asignado a la petición quedará bloqueado hasta que la tarea finalice.

Imaginemos, por ejemplo, el siguiente controlador:
public ActionResult ShowTheAnswer()
{
 var result = _oracle.GetTheAnswerToLifeTheUniverseAndEverything();
 return View(result);
}
Si la llamada a GetTheAnswerToLifeTheUniverseAndEverything() tardase, digamos 10 segundos, obviamente el usuario no vería el resultado hasta ese momento, pero, además, estaríamos desperdiciando un valioso recurso: el hilo asignado por IIS, que permanecería todo ese tiempo esperando a que el método realizara su trabajo. Probablemente, durante estos 10 segundos el mismo hilo podría haber atendido muchas otras peticiones.

Y es ahí donde se encuentra la gracia de los controladores asíncronos. Permiten “liberar” el hilo del servidor web para que pueda procesar otras peticiones mientras se espera el resultado de la ejecución de la tarea conflictiva. Una vez ésta ha finalizado, el servidor asignará otro hilo para que procese el resultado obtenido y finalmente retorne el resultado al cliente.

Al igual que antes, el usuario seguirá sin ver nada en su pantalla hasta transcurridos los 10 segundos, la diferencia está en que el servidor estará aprovechando mucho más los threads disponibles y, por tanto, será capaz de gestionar muchas más peticiones. Aunque en aplicaciones con poca carga normalmente no necesitaremos utilizarlos, sí suponen una diferencia importante cuando estamos hablando de dar servicio a una gran cantidad de usuarios concurrentes.

Acciones asíncronas en MVC 4

Los controladores asíncronos están disponibles desde las primeras versiones de ASP.NET MVC y, aunque permitían aplicar esta técnica, resultaban bastante tediosos de utilizar, pues parte de la gestión de la asincronía debíamos implementarla de forma manual. Podéis ver ejemplos aquí (Gisela Torres) o  aquí (Maxi Lovera).

A partir de MVC 4, y gracias a que la asincronía está siendo incorporada de forma masiva en el framework, podemos hacerlo de forma muchísimo más sencilla. Básicamente sólo tenemos que seguir cuatro pasos si usamos Visual Studio 2012 y ASP.NET 4.5:
  • Hacer que la acción retorne un Task<T>, donde T será normalmente del tipo ActionResult o alguno de sus descendientes.
  • Introducir en la declaración del método de acción la palabra clave async de C#, indicando de esta forma que se va a invocar un proceso asíncrono cuyo resultado obtendremos con await. Si no has oído hablar antes de estas dos novedades de la versión 5 de nuestro lenguaje favorito, puedes leer este post del gran Eduard Tomás donde las explica perfectamente.
  • Opcionalmente, si queremos seguir las convenciones de nombrado para métodos asíncronos, debemos añadir a su nombre el sufijo “Async”.
  • Por último, ya en el cuerpo de la acción, llamar a los métodos asíncronos usando await.
Nota: para conseguir lo mismo con Visual Studio 2010 es necesario instalar el Visual Studio Async CTP, aunque por lo que he podido comprobar no es una tarea sencilla.

Vamos a ilustrarlo con un ejemplo. El siguiente código muestra la versión síncrona, o tradicional, de un controlador, así como un método del Modelo que realiza un trabajo de larga duración:
// Controller
public class OracleController : Controller
{
    [...]
    public ActionResult ShowTheAnswer()
    {
        var result = _oracle.GetTheAnswerToLifeTheUniverseAndEverythingAsync();
        return Content("Response: " + result);
    }
}

// Model
public class Oracle
{
    [...]
    public int GetTheAnswerToLifeTheUniverseAndEverything()
    {
        Thread.Sleep(10000); // Simulate a very hard task
        return 42;
    }
}
Y a continuación mostramos la versión asíncrona de la misma acción y el correspondiente método del Modelo:
// Controller
public class OracleController : Controller
{
    [...]
    public async Task<ActionResult> ShowTheAnswerAsync()
    {
        var result = await _oracle.GetTheAnswerToLifeTheUniverseAndEverythingAsync();
        return Content("Response: " + result);
    }
}

// Model
public class Oracle
{
    [...]
    public Task<int> GetTheAnswerToLifeTheUniverseAndEverythingAsync()
    {
        return Task.Factory.StartNew(() =>
        {
            Thread.Sleep(10000); // Simulate a very hard task
            return 42;
        });
    }
}
Desde el punto de vista del Controlador, la llamada al método del Modelo retornará un objeto de tipo Task que representará a la tarea que ha comenzado a realizarse en segundo plano. El uso de await hará que el thread del servidor web sea liberado, y que se vuelva a tomar el control en este punto cuando la tarea haya finalizado y ya tengamos un resultado disponible.

Por tanto, podríamos generalizar el patrón para la implementación de acciones asíncronas que retornan una vista de la siguiente forma:
public async Task<ActionResult> MyAction(param1, param2…)
    {
        var result = await AsyncOperation();
        return View(result);
    }

¿Más de una tarea asíncrona desde el interior de la acción?

Si estamos ante un escenario en el que necesitamos ejecutar más de una tarea asíncrona en la misma acción, podemos optar por hacerlo de forma secuencial o paralelizar también estas tareas.

El primer enfoque, bastante trivial, consistiría en realizar las llamadas una detrás de otra. Por ejemplo, volviendo sobre el ejemplo anterior, si quisiéramos obtener una segunda opinión sobre las verdades del universo, podríamos hacer como sigue:
public async Task<ActionResult> ShowTheAnswerAsync()
{
    var result1 = await _oracle1.GetTheAnswerToLifeTheUniverseAndEverythingAsync();
    var result2 = await _oracle2.GetTheAnswerToLifeTheUniverseAndEverythingAsync();
    return Content("Response: " + (result1+result2)/2);
}
La primera llamada al método asíncrono retornará el Task<int> representando a la tarea que acabará de comenzar a ejecutarse, y la palabra clave await hará que el hilo quede liberado hasta que se obtenga la primera respuesta, momento en que se volverá a tomar el control para introducirla en result1. A continuación se hará lo mismo para obtener la segunda opinión.

El tiempo total de ejecución, desde que la acción comienza a ejecutarse hasta que el resultado es enviado al cliente será de 20 segundos, asumiendo que cada llamada tarda 10 segundos.

La otra posibilidad es paralelizar ambas llamadas. En este caso, lanzamos las dos tareas en paralelo, obteniendo las referencias hacia las mismas, y usamos awaitWhenAll()  para esperar la finalización de ambas:
public async Task<ActionResult> ShowTheAnswerAsync()
{
    var task1 =  _oracle1.GetTheAnswerToLifeTheUniverseAndEverythingAsync();
    var task2 =  _oracle2.GetTheAnswerToLifeTheUniverseAndEverythingAsync();
    await Task.WhenAll(task1, task2);
    return Content("Response: " + (task1.Result+task2.Result)/2);
}
En este caso, dado que ambas tareas se realizan de forma simultánea, el resultado será mostrado al usuario en 10 segundos.

That's all, folks!

Bueno, hay más cosas que contar, pero de momento vamos a dejarlo aquí :-)

La implementación de acciones asíncronas para la realización de tareas de cierta duración aporta grandes beneficios en sistemas de alta concurrencia, y con ASP.NET MVC 4 resulta realmente sencillo.

Su uso está recomendado cuando la acción vaya a realizar una tarea de larga duración e involucre el acceso a recursos externos. Es decir, si el proceso a realizar es costoso pero de uso intensivo de CPU, no se obtendrá un beneficio destacable.

Más información en: Using Asynchronous Methods in ASP.NET MVC 4


Publicado en: Variable not found.

7 Comentarios:

Örjan dijo...

Realmente sólo veo un cambio de sintaxis que no creo que aporta demasiado. De acuerdo que encaja mejor con el estilo actual de utilizar "generics" e interfaces fluidas pero no lo considero más cómodo que lo disponible actualmente.

El clásico patrón "Asynchronous Method Invocation" (AMI) aplicado en .NET, con su "Invoke", "BeginInvoke" y "callback" siempre me ha parecido más que suficiente. Si ya se busca una mayor simplificación se puede utilizar el patrón "Event-Based Asynchronous".

Supongo que esta nueva sintaxis resultará más usable a quienes no conocen demasiado la programación asíncrona. Los desarrolladores del framework de .NET cuidan bastante estos temas.

Veremos qué tal va cuando la pruebe. Igual obra el milagro y termino por adoptar esta sintaxis.

josé M. Aguilar dijo...

Hola, Örjan.

Ante todo, gracias por comentar :-)

Está claro que es puro "azucarillo sintáctico", es decir, no hay ningún beneficio más allá de simplificar la sintaxis, puesto que internamente el funcionamiento va a ser similar.

Pero lo que es indudable es que el código queda más claro y conciso, y eso es bueno tanto para el que lo escribe como para el que lo lee; y por supuesto, eso lleva implícito una menor probabilidad de cometer errores.

Aparte de eso, quizás la principal ventaja en MVC sea que la asincronía ha sido llevada a nivel de acción y no de controlador, como ocurría en versiones anteriores. Es decir, ahora seguiremos heredando de Controller y simplemente implementaremos de forma asíncrona las acciones en las que sea conveniente, sin necesidad de crear un controlador específico para ellas.

Saludos!

Juanma dijo...

Muy buena explicación.

Una duda sobre cómo se implementa esto por debajo:

Para esperar a que se complete la operación asíncrona, ¿se usan hebras del ThreadPool? ¿Se usan completion ports? ¿Hay una sola hebra con un Wait sobre un montón de WaitHandles que luego invoca el callback adecuado?

josé M. Aguilar dijo...

Hola, Juanma!

Pues no te lo sabría decir con seguridad... de hecho, puedes encontrar información por ahí que sugieren el uso de completion ports, pero también de hilos independientes para esperar las peticiones.

Sin duda es un bonito tema para indagar un domingo lluvioso por la tarde ;-)

Saludos & gracias por comentar.

Sergio León dijo...

Hola Jose:

Una pregunta ¿No debería heredar el controlador de AsyncController? Yo creo que sí, pero ya me haces dudar...

Un saludo.

josé M. Aguilar dijo...

Hola!

Sí, hasta mvc3 era necesario si querías usar asincronía, pero desde el 4 viene ya de serie :-)

Saludos.,

Sergio León dijo...

OK, muchas gracias, entonces me olvido de AsyncController y simplemente hacemos "acciones" asíncronas.
Un saludo y gracias.