Uso de async/await en C#

Introducción

A partir de C# 4.5, se incorporaron al lenguaje las palabras clave async y await, que facilitan la escritura de código asíncrono. Esto no significa que con solo poner estas palabras el código se ejecutará de manera asíncrona. Es un poco mas complejo, y lo veremos en este post.

Esta guia también esta disponible en forma de video, si asi lo preferís:

La forma antigua

Los que hemos trabajado con Winforms (y tambien los programadores Java) sabemos del gran problema de llamar a operaciones largas desde el hilo principal. Como este es el hilo donde corre la UI, si llamamos a una operación larga se bloquea y no es posible ni siquiera mover la ventana.

Para estos casos podemos hacer ciertas cosas, según la complejidad de la operación a realizar:

Cambiar el cursor

Si cambiamos el cursor del mouse a un cursor de espera, por ejemplo, al hacer:

this.UseWaitCursor=true;
MetodoNoMuyLargo();
this.UseWaitCursor=false;

le daremos al usuario un feedback visual de que una operación larga esta ocurriendo, y que debe esperar. Este método es válido cuando la operación no es tan larga, y podemos estar relativamente seguros de que no va a fallar. Por ejemplo, se puede usar a la hora de abrir o guardar un archivo. Aunque si el archivo es muy largo, quizá sea conveniente usar el segundo método.

BackgroundWorker

Esta es la clase que se usaba (y se puede seguir usando, claro) en versiones anteriores de .NET. Esta orientada al diseño clasico con el diseñador de Visual Studio: Se arrastra el componente desde la caja de herramientas, y se asignan los eventos.

Aquí un ejemplo muy sencillo:

public class BWDemo
    {
        BackgroundWorker bw;
        public BWDemo()
        {
            InitializeComponent();
            bw = new BackgroundWorker();
            bw.DoWork += Bw_DoWork;
            bw.RunWorkerCompleted += Bw_RunWorkerCompleted;
        }

        private void Bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            label1.Text = "Finalizado";
        }

        private void Bw_DoWork(object sender, DoWorkEventArgs e)
        {
            LlamadaAMetodoLargo();
        }
        private void button1_Click(object sender, EventArgs e)
        {
            bw.RunWorkerAsync();
        }
    }

En este caso estamos creando un BackgroundWorker y asignando 2 eventos: DoWork, que es el que se ejecuta en otro hilo; y RunWorkerCompleted, que es el que maneja los resultados de la ejecución y los procesa en el hilo principal.

Si bien esta opcion todavia se puede usar, presenta varios problemas. Principalmente, si es necesario realizar multiples tareas, y se necesita esperar el resultado de una para continuar con la otra, es necesario anidar los BackgroundWorkers y el codigo se vuelve inmantenible muy rápidamente. No vamos a seguir ahondando en BackgroundWorker porque Microsoft ya no recomienda su uso. En su lugar, ahora tenemos:

La forma moderna: async/await

Se puede hablar muchísimo sobre async y await, y la forma como funcionan por dentro. Pero me costó bastante entenderlo, asi que mejor empecemos con un ejemplo. Supongamos que tenemos que realizar una operación larga al hacer click en un botón. Sería tan sencillo como esto:

private async void button1_Click(object sender, EventArgs e)
{
   await Task.Run(() => { OperacionMuyLarga(); });
}

A simple vista, tenemos un evento Click común y corriente. Pero podemos ver que se agregó la palabra clave async a la declaración. Es importante mencionar que esta palabra clave no altera la signatura del método. Es decir, no puedo tener dos métodos idénticos, uno con async, y el otro sin async, ya que el compilador nos dará el error de que existen métodos idénticos.

Mas abajo tenemos dos cosas: la palabra clave await, y una llamada a Task.Run() con un lambda.

Aquí es donde se ve lo que hace async y await: async solamente declara que el método se ejecutará asíncronamente. No lo llama asíncronamente, sino que hace que se habilite el uso de await dentro del cuerpo del método.

El método que se ejecuta debe ser un Task, y esta Task es la que se ejecutará de manera asíncrona.

Retornar valores desde una llamada asíncrona

Aquí es donde async/await brilla en comparación con BackgroundWorker. Antes solo podíamos retornar un object con el resultado de la operación, lo que hacía del refactoring de código una pesadilla: como los metodos estaban desacoplados, habia que hacer un cast, y si olvidábamos el cast en RunWorkerCompleted, solo descubríamos el error al ejecutar el programa y obtener InvalidCastException.

Para devolver un valor, el tipo Task es genérico. Podemos devolver cualquier cosa. Un ejemplo, un poco mas complejo.

private async void button1_Click(object sender, EventArgs e)
{
    var result = await Task.Run<int>(() => { return LongOperation(); });
    label1.Text = result.ToString();
}

int LongOperation()
{
    System.Threading.Thread.Sleep(3000);

    return 25;
}

Aquí vemos dos cosas: agregamos el tipo int Task, para indicar que la tarea devuelve un entero, y dentro del lambda, ahora, estamos usando la palabra clave return. También podemos ver que declaramos una variable result, que recibira el valor de Task. Esta variable esta declarada como var, y tomara el tipo definido en el genérico de Task.

Manejo de Excepciones dentro de Task

¡El manejo de excepciones cuando usamos async/await en vez de BackgroundWorker es trivial!

private async void button1_Click(object sender, EventArgs e)
{
    try
    {
        var result = await Task.Run<int>(() => { return LongOperation(); });
        label1.Text = result.ToString();
    }
    catch (Exception ex)
    {
        label1.Text = "Hubo un error: " + ex.Message;
    }
}

Simplemente manejamos la excepción como cualquier otra excepción. Con BackgroundWorker debíamos, antes que nada, mirar si el parametro e.Error era distinto de null. Si no era null, manejabamos la excepción. Con async/await tratamos los métodos como si fueran cualquier otro. Tan sencillo como eso.

Tareas secuenciales

Veamos el siguiente código:

var result1 = await Task.Run<int>(() => { return LongOperation1(); });
label1.Text = "Se ejecuto LongOperation1. Ejecutando LongOperation2...";
var result2 = await Task.Run<int>(() => { return LongOperation2(result2); });
label1.Text = "Se ejecuto LongOperation2. Ejecutando LongOperation3...";
var result3 = await Task.Run<int>(() => { return LongOperation3(result3); });
//... y así.

Es así de sencillo.

Deja un comentario

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