Cuidado con los async void

A menudo veo el uso de la palabra clave async en métodos void, y me pregunto ¿por que? si un método void no es awaitable y no nos vamos a poder enterar de cuando una tarea termine, ¿por que es posible marcar como async un metodo void?. Antes veamos algo de código para entrar en contexto.

Teniendo en cuenta el siguiente código:

        private void Button1Click(object sender, EventArgs e)
        {
            try
            {
                AsyncVoid();
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Exception : {0}", ex);
            }
            Debug.WriteLine("Fin llamador");
        }

        public async void AsyncVoid()
        {
            var task = SuperCalcular();
            var resultado = await task;
            Debug.WriteLine("Fin metodo asincrono, resultado: {0}", resultado);
        }

        public async Task SuperCalcular()
        {
            var task = Task.Delay(4000);
            await task;
            return 3.14;
        }

El resultado será una salida como:

Fin llamador
Fin metodo asincrono, resultado: 3.14

Vemos que se inicia con la llamada a nuestro método AsyncVoid donde una vez se encuentra con el await retorna al llamador quien sigue con su normal ejecución, una vez el método SuperCalcular finaliza, nuestro método AsyncVoid continua su ejecución,  recuperamos el resultado y hacemos algo con este. Pero aquí hay dos problemas, el método llamador no podrá saber cuando el método AsyncVoid termine su ejecución y será imposible capturar una excepción generada por el método AsyncVoid desde el método llamador, para comprobar este segundo punto basta con editar el método AsyncVoid así:

        public async void AsyncVoid()
        {
            var task = SuperCalcular();
            var resultado = await task;
            Debug.WriteLine("Fin metodo asincrono, resultado: {0}", resultado);
            throw new Exception("Excepcion generada");
        }

Veremos entonces que la excepción no se puede controlar y es lanzada en el UI message-loop. Para solventar este problema podemos implementar un primer approach, algo como:

        public void AsyncVoid()
        {
            var task = SuperCalcular();
            task.ContinueWith(resultado => Debug.WriteLine("Fin metodo asincrono, resultado: {0}", resultado.Result));
            throw new Exception("Excepcion generada");
        }

Con esto obtendremos una salida como:

Exception : System.Exception: Excepcion generada
   at WindowsFormsApplication1.Form1.AsyncVoid()
Fin llamador
Fin metodo asincrono, resultado: 3.14

Pero con esto solo solucionamos uno de los problemas y además no empleamos las útiles abstracciones de async / await  y tenemos nuevamente que empezar a lidiar con tareas a pedal.

Entonces veamos una solución mas integral:

        private async void Button1Click(object sender, EventArgs e)
        {
            try
            {
                await AsyncVoid();
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Exception : {0}", ex);
            }
            Debug.WriteLine("Fin llamador");
        }

        public async Task AsyncVoid()
        {
            var task = SuperCalcular();
            var resultado = await task;
            Debug.WriteLine("Fin metodo asincrono, resultado: {0}", resultado);
            //throw new Exception("Excepcion generada");
        }

Ahora tenemos un async Task, este si es awaitable (a diferencia de el async void) pero no podemos esperar un resultado de este, es decir, es ilegal hacer var task = await AsyncVoid(); lo que nos permite realmente es poder ejecutar nuestro código una vez el se termine la ejecución de las tareas. La salida se ve así:

Fin metodo asincrono, resultado: 3.14
Fin llamador

ok, pero no me interesa esperar a que se finalice lo que sea que haga el método AsyncVoid, bien, pues puedes emplear algo como var task = AsyncVoid();  y comprobar luego el estado del objeto task, entonces algo como:

        private async void Button1Click(object sender, EventArgs e)
        {
            Task task = null;
            try
            {
                task = AsyncVoid();
                Debug.WriteLine("Otra pesada funcionalidad...");
                Task.Delay(1000);
                Debug.WriteLine("Sigo trabajando...");
            }
            catch (Exception ex)
            {
                Debug.WriteLine("Exception : {0}", ex);
            }
            Debug.WriteLine(task.Status);
            await task;
            Debug.WriteLine(task.Status);
            Debug.WriteLine("Fin llamador");
        }

Producirá la siguiente salida:

Otra pesada funcionalidad…
Sigo trabajando…
WaitingForActivation
Fin metodo asincrono, resultado: 3.14
RanToCompletion
Fin llamador

Ok, Pero entonces, ¿por que es legal tener async void?… bien, pues async void debe ser unicamente empleado en cuando se trabaje con eventos, (como se ve en el código anterior) porque es algo que se acciona pero nunca se espera! un async void no es awaitale y un evento no necesita que lo sea.

En channel9, Lucian Wischik explica en este video a profundidad lo que aquí he tratado de explicar, no dejen de verlo.

Conclusión

No uses async void si no es un evento, sencillo 😛

Espero les sea de utilidad.

Hasta el próximo post.

Anuncios
Cuidado con los async void

2 comentarios en “Cuidado con los async void

  1. El evento de por si es un delegado ejecutándose asíncronamente, es decir de una u otra forma ya es un Async, y como lo dices en un evento si puede ser necesario que un Async se ejecute y esperar a que finalice, para ello hay dos opciones (que me aprecen mucho más sencillas):
    1. marcarlo el evento async y así poder llamar al método Async con await
    2. No marcarlo e invocar el método que a la final es un Task, y de un Task podemos preguntar (forzar) por Task.Result al invocar Task.Result sobre un método Async fuerzas que espere (await) hasta que retorne el valor de result.

    —–
    “Veremos entonces que la excepción no se puede controlar y es lanzada en el UI message-loop”

    public async void AsyncVoid()
    {
    var task = SuperCalcular();
    var resultado = await task;
    Debug.WriteLine(“Fin metodo asincrono, resultado: {0}”, resultado);
    throw new Exception(“Excepcion generada”);
    }

    No se lanza en el UI message loop, solo piensalo que pasa si mi programa es un servicio sin UI? dónde se lanza la excepción? la excepción se lanza en el hilo que se esta ejecutando…

    a estas alturas en el programa del ejemplo esta el hilo ppal (#1), luego el hilo donde se ejecuta el evento(#2) y luego dentro de este se dispara el hilo del llamado a AsyncVoid() (#3) allí inicia la fase terminal del hilo(#2) dónde se disparo el evento mientras AsyncVoid continua con su ejecución.

    Dentro de AsyncVoid() llamas a SuperCalcular (Task) otro hilo más (#4) y al usar await estas indicando que una vez finalice haga callback de un delegado con las dos líneas siguientes

    Debug.WriteLine(“Fin metodo asincrono, resultado: {0}”, resultado);
    throw new Exception(“Excepcion generada”);

    Ese callBack (que es lo que genera el await) no es más que otro método asíncrono (otro hilo) (#5) ejecutándose al final del Task, conclusión : la excepción se genera desde el hilo de #5 que no necesariamente pertenece al UI message Loop.

    Esta excepción no revienta el programa porque aunque no es controlada simplemente no pertenece el hilo principal, el hilo que muere es el #5 y siempre que los objetos compartidos no se vean afectados por la excepción, el programa principal sigue su curso.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s