Programación asíncrona en C#

En este post veremos que es la programación asíncrona y sobre todo, cuándo y dónde usar programación asíncrona.

 

1 - Qué es la programación asíncrona? 

Como su nombre indica nos permite crear código que se va a ejecutar de una forma paralela.

Es muy importante no confundirlo con TPL (Task parallelization) ya que pese a que son conceptos similares, no son el mismo. 

Un fallo muy común cuando empezamos a programar es pensar que más Task significa multihilo, o que cada Task será un hilo nuevo (Threads), cuando no tiene porqué ser siempre así. Cuando invocamos Task estamos invocando la librería Task parallel la cual se va a encargar de toda la administración de los hilos por nosotros. 

La gran ventaja de utilizar async / await es que permite escribir de una forma muy fácil y sencilla código que se ejecuta de forma paralela. 

Voy a intentar escribir el post con un ejemplo real y así siento que se entenderá de una forma más sencilla. 

 

2 - Cómo utilizar programación asíncrona en C# con Async / await

Lo primero es ver cómo escribimos código para que sea asíncrono, El codigo esta disponible en GitHub y voy a usar el ejmplo visto en el Post de como conectarse a una base de datos .

 Disponemos del siguiente código que hemos decidido convertir en asíncrono: 

public Articulo GetArticulo(int id)
{
    using(MySqlConnection conexion = new MySqlConnection(conexionString))
    {
        //Codigo para el select
        conexion.Open();
        MySqlCommand comando = new MySqlCommand();
        comando.CommandText = "select * from articulo where id = ?articuloId;";
        comando.Parameters.Add("?articuloId", MySqlDbType.Int32).Value = id;
        comando.Connection = conexion;

        Articulo articulo = new Articulo();
        using (var reader = comando.ExecuteReader())
        {
            while (reader.Read())
            {
                articulo.Id = Convert.ToInt32(reader["Id"]);
                articulo.Titulo = reader["Titulo"].ToString();
                articulo.Contenido = reader["Contenido"].ToString();
            }

            return articulo;
        }
    }
}

Como podemos observar es el código de consultar en la base de datos. La forma de convertir este método a asíncrono es muy fácil, para ello únicamente debemos indicar que en vez de devolver una clase Articulo nos devuelve Task<T>  donde como vimos en generics  T es Articulo.

public Task<Articulo> GetArticulo(int id){...}

Como vemos nos indicará varios errores, el primero es que debemos utilizar la librería System.Threading.Tasks y el segundo es que no estamos utilizando las tareas dentro de nuestro método

Si hemos indicado el método con Task<T> es porque queremos que corra de una forma asíncrona. En este escenario podemos llamar a la base de datos de forma asíncrona, vamos a ver como.

Lo primero es abrir la conexión, lo cual lo podemos hacer de forma asíncrona, actualmente llamamos con la siguiente sintaxis conexion.Open();. Una forma muy común cuando se escriben librerías es que si un método es asíncrono, le ponemos async al final del mismo, por lo tanto, podemos abrir la conexión a la base de datos con conexion.OpenAsync();` . pero este método NO abre la conexión como tal, para abrir la conexión debemos esperar a que se abra. para ello utilizaremos la palabra clave await. Por lo que luce de la siguiente manera:

await conexion.OpenAsync();

Cuando utilizamos await, debemos utilizar la palabra clave async en la cabecera del método, por lo que el método luce de la siguiente manera:

public async Task<Articulo> GetArticulo(int id){...}

Y de esta forma ya tenemos nuestro método asíncrono:

public async Task<Articulo> GetArticulo(int id)
{
    using(MySqlConnection conexion = new MySqlConnection(conexionString))
    {
        //Codigo para el select
        await conexion.OpenAsync();
        MySqlCommand comando = new MySqlCommand();
        comando.CommandText = "select * from articulo where id = ?articuloId;";
        comando.Parameters.Add("?articuloId", MySqlDbType.Int32).Value = id;
        comando.Connection = conexion;

        Articulo articulo = new Articulo();
        using (var reader = await comando.ExecuteReaderAsync())
        {
            while (reader.Read())
            {
                articulo.Id = Convert.ToInt32(reader["Id"]);
                articulo.Titulo = reader["Titulo"].ToString();
                articulo.Contenido = reader["Contenido"].ToString();
            }

            return articulo;
        }
    }
}

Pero realizar el método de forma asíncrona no es suficiente, cuando utilizamos Task<> en una aparte del proceso debemos ejecutar todo el proceso de forma asíncrona, por lo que todos los métodos del proceso deberán contener Task<T>.

Como por ejemplo el método que llama a nuestro método que acabamos de modificar, lo convertiremos de: 

public Articulo ConsultarArticulo(int id)
{
    return _articuloRepository.GetArticulo(id);
}

Al siguiente:

public async Task<Articulo> ConsultarArticulo(int id)
{
    return await _articuloRepository.GetArticulo(id);
}

y como este, todos los métodos que nuestro proceso asíncrono esta utilizando.

 

2.1 - Await en C#

Como acabo de comentar para recibir el objeto que deseamos de un método que devuelve Task<T> debemos esperar y para ello indicamos la palabra clave await.

pero debemos ser cuidadosos, ya que podemos convertir nuestro código asíncrono en sincrono de una manera muy sencilla. Por ejemplo el siguiente código:

Articulo articulo1 = await _articuloRepository.GetArticulo(1);
Articulo articulo2 = await _articuloRepository.GetArticulo(2);
Articulo articulo3 = await _articuloRepository.GetArticulo(3);

Ejecuta las instrucciones una por una y en orden, espera a que la primera con el id 1 termine, para empezar la segunda y así sucesivamente.

Para ejecutar dichas acciones de forma asíncrona debemos llamar al método que devuelve Task, y una vez tenemos esta Task en una variable, hacer el await.

Task<Articulo> taskArticulo1 =  _articuloRepository.GetArticulo(1);
Task<Articulo> taskArticulo2 =  _articuloRepository.GetArticulo(2);
Task<Articulo> taskArticulo3 =  _articuloRepository.GetArticulo(3);

Articulo articulo3 = await taskArticulo3;
Articulo articulo2 = await taskArticulo2;
Articulo articulo1 = await taskArticulo1;

 

2.2 - Método Task.WhenAll en C#

Alternativamente al caso que acabamos de ver cuando tenemos múltiples tareas para ejecutar. C# nos provee un método llamado Task.WhenAll(IEnumerable<Task>) el cual como observamos nos permite indicar una lista de Task para ejecutar. 

Task<Articulo> taskArticulo1 = _articuloRepository.GetArticulo(1);
Task<Articulo> taskArticulo2 = _articuloRepository.GetArticulo(2);
Task<Autor> taskAutor1 = _autorRepository.GetAutor(1);

_ = Task.WhenAll(taskArticulo1, taskArticulo2, taskAutor1);

Articulo articulo1 = taskArticulo1.Result;
Articulo articulo2 = taskArticulo2.Result;
Autor autor1 = taskAutor1.Result;

Como podemos observar, podemos introducir cualquier tipo de Task dentro de nuestra lista, y posteriormente accedemos al resultado con la propiedad .Result;

nota: si intentamos acceder a la propiedad .Result antes de esperar con await, no podremos, ya que la tarea no estará ejecutada. 

 

Debemos priorizar la utilización de Task.WhenAll sobre la forma de esperar varias veces y esto es por varios motivos:

  • El código luce más limpio.
  • Propaga los errores correctamente, si tenemos, 10 tareas con await, y uno de los primeros falla puedes perder el error. 
  • Utilizar WhenAll espera hasta que TODAS las tareas terminan, incluso si hay errores. es posible que, programando tu, tengas un try{}cath() y si uno falla, saltes una excepción, en ese caso parte de tu código querrá ir a la excepción y otra parte esperar, y puede dar errores y cuelgues. 

 

3 - Cuándo utilizar programación asíncrona en c#

Debemos utilizar programación asíncrona siempre que podamos, así de claro, los beneficios que nos trae son muy buenos, sobre todo en rendimiento y respuesta.

Podemos ver un ejemplo muy claro, si llamamos, desde nuestro programa a digamos 3 apis, y lo hacemos de una en una forma síncrona, el gráfico de la llamada es similar al siguiente: 

llamadas sincronas

 

Como vemos las llamadas a los 3 servicios externos llevan un total de 15.5 segundos.

 

Mientras que si hacemos las llamadas de forma asíncrona tardamos 6 segundos.

llamadas asíncronas

Esta lógica, la podemos aplicar para todos los servicios, por ejemplo una llamada a la base de datos, si tienes que consultar diferentes tablas, puedes hacer todas las llamadas de forma asíncrona. 

 

Conclusión

Realizar todas las llamadas de una forma asíncrona hace que nuestro código sea mucho más rápido, ya que si realizamos múltiples llamadas a servicios externos por ejemplo, no tenemos que esperar una a una a que nos las devuelvan, sino que esperaremos a que todas estén ya en nuestro poder. 

Utilizar async / await hace que nuestro código sea mucho más claro y limpio que utilizando hilos, que sinceramente es un jaleo.