Métodos asíncronos en c#

Desde hace tiempo me ronda en la cabeza escribir algo acerca de la programación asíncrona.  ¿Quién no ha desarrollado aplicaciones que se  quedaban colgadas/ congeladas mientras que se estaba ejecutando una acción pesada? 

Un ordenador tiene un número determinado de procesadores y cada uno de ellos ejecuta únicamente una operación a la vez. Los métodos síncronos son bloqueantes debido a que el hilo que llama al método no hace otro trabajo hasta que el método se completa, por lo que para evitar lo que en muchos casos es un cuello de botella aparecen los métodos asíncronos.

En la versión 4 del framework de .NET se introdujo el espacio de nombres System.Threading.Tasks como un nuevo acercamiento a la programación asíncrona, dónde la clase  System.Threading.Tasks.Task<TResult> representa una tarea que se completará en un futuro. El aspecto de nuestros métodos asíncronos es de esta forma.

El objeto Task<TResult> devuelto expone un método GetAwaiter que devuelve un TaskAwaiter<TResult> que sirve para poder anexar un delegado que se ejecutará cuando finalice la tarea .

Desde el la versión 4.5 del framework de .NET tenemos disponibles dos nuevos términos (keywords) para poder llevar a cabo la programación asíncrona de una forma más sencilla.

  • async. Es un modificador para los métodos que indica que su ejecución se puede llevar a cabo después de que el método que haga la llamada haya terminado.
    Un método asíncrono puede devolver void, Task o el genérico de este último Task<TResult>.
  • await.  Una expression await solo es posible en el contexto (keyword contextual) de un método marcado con el modificador async. El método corre de forma síncrona hasta que se encuentra con la primera expresión await, momento en el cual la ejecución se suspende hasta que la tarea esperada se complete.

Este nuevo escenario también es conocido como TAP (short for Task-based Asynchronous Pattern) y su “patrón” de uso es:

El compilador lo traduce como:

Escenario inicial

Dejamos de lado la teoría e intentemos plantear un escenario “real” dónde veamos que ventajas nos ofrece la programación asíncrona. Imaginemos una aplicación de consola que ejecuta un método con dos operaciones absolutamente bloqueantes (Thread.Sleep de dos y de tres segundos). Si mientras las operaciones están en proceso intentamos pular la tecla Intro para salir de la aplicación observamos que no hace nada hasta que finaliza dichos procesos.

El resultado de este proceso es el siguiente:

Todo se ejecuta en el mismo Thread (9), el resultado de la operación que es la suma de los segundos de una (2) y otra operación (3) es igual a 5 y la duración del proceso en segundos ha sido algo superior a esos 5 segundos.  Si hemos intentado escribir cualquier tecla o pulsar Intro para salir de la aplicación vemos que no ha hecho nada hasta que han terminado las operaciones.

Primera solución asíncrona

Vamos a crear dentro de la misma clase Operaciones, los métodos asíncronos para estos dos métodos (DosSegundos y TresSegundos). Los aspectos que debemos tener en cuenta son:

  • El valor devuelto por el la función asíncrona es del tipo Task<TResult>, Task o void.
  • Task.Run. Pone en cola el trabajo especificado para ejecutarlo en el ThreadPool y devuelve un controlador Task<TResult> para dicho trabajo.
  • La nomenglatura de un método asíncrono suele ser el nombre del método síncrono acompañado de la palabra Async.

También crearemos un nuevo método EjecutarAsync.

El resultado de este proceso sería el siguiente:

La aplicación de consola se ejecuta en el Thread (9) mientras que las operaciones se ejecutan en el Thread(6) y en el Thread(10), el resultado de la operación que es la suma de los segundos de una (2) y otra operación (3) es igual a 5 y la duración del proceso en segundos ha sido algo superior a esos 5 segundos.

En resumidas cuentas han cambiado dos cosas:

  • Si pulsamos cualquier tecla o la tecla Intro durante la ejecución vemos que la aplicación no se encuentra colgada ni congelada. Interesante ¿Verdad?
  • El thread dónde se ejecutan las operaciones respecto al que se ejecuta la aplicación de consola son distintos pero la duración del proceso sigue siendo la misma (algo más de 5 segundos). ¿Es posible bajar este tiempo?

Segunda solución asíncrona

Ahora la idea es que ambas operaciones (DosSegundosAsync y TresSegundosAsync) en vez de tener un await individual lo tengan común, para ello utilizamos el método WhenAll de la clase Task y de esa forma las dos operaciones se ejecutarán en paralelo.

El resultado de este proceso sería el siguiente:

La aplicación de consola se ejecuta en el Thread (9) mientras que las operaciones se ejecutan en los Threads 9, 10 y 11. El resultado de la operación que es la suma de los segundos de una (2) y otra operación (3) es igual a 5 y la duración del proceso en segundos ha sido ahora algo superior a 3 segundos.

¿Cómo puede ser? ¿Cómo es posible que el proceso total tarde un poco más que la operación que más tarda? La aplicación de consola  se han ejecutado en el Thread 9 y las dos operaciones se han ejecutado en pararelo, la operación DosSegundos en el Thread 10 y la operación TresSegundos  en el Thread 11.

El código completo de todo lo visto en este ejemplo lo tienes disponible en GitHub.

Agradecer a panicoenlaxbox su aporte y conocimiento para completar este post.

Este post es un resumen de este artículo de la MSDN.

twitter Métodos asíncronos en c#9google Métodos asíncronos en c#1facebook Métodos asíncronos en c#2linkedin Métodos asíncronos en c#8

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

Puedes usar las siguientes etiquetas y atributos HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">