Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

async y await en C#: cómo manejar asincronismo en .Net de manera fácil

Foto CC0 por Amirali Mirhashemian en Unsplash

Una de las cosas que más te pueden frustrar cuando empiezas en el mundo de la programación es ver que, cuando a base de mucho esfuerzo has conseguido que tu programa funcione, el resultado es que funciona... pero se bloquea la interfaz, no escala bien, etc.

Has oído o leído algo sobre "hilos de ejecución" y te decides a probarlos, pero ves que tienes que manejarlos, que sincronizarlos, preguntarles qué tal están de vez en cuando... Un trabajo tedioso y que muchas veces no es necesario porque existe una herramienta para ello.

Este es el caso del asincronismo que conocemos hoy en día. Desde hace ya mucho tiempo (desde la versión 5.0 de C#) tenemos a nuestra disposición 2 palabras clave que nos permiten manejar estas situaciones multi-hilo con una lógica y una sintaxis verdaderamente asíncrona.

En realidad el asincronismo estaba disponible antes con los métodos .Begin() y .End() que implementaban algunas clases, pero era difícil de seguir y de mantener, o también mediante el uso de la clase Task y su método ContinueWith().

Estas dos palabras reservadas, en combinación con la clase genérica Task, nos permiten disponer de una sintaxis mucho más fluida en nuestro código. Tan solo es necesario que añadamos async en la firma de los métodos asíncronos, y que esperemos el retorno donde proceda con await, así:

//Asíncrono C# >= 5
public async Task<int> ExecuteCommandAsync(string command)
{
    using (SqlCommand sqlCommand = new SqlCommand(command, sqlConnection))
    {
        return await sqlCommand.ExecuteNonQueryAsync();
    }
}

//Asíncrono C# 4
public int ExecuteCommandAsync(string command)
{
    using (SqlCommand sqlCommand = new SqlCommand(command, sqlConnection))
    {
        var handler = sqlCommand.BeginExecuteNonQuery();
        //.....
        return sqlCommand.EndExecuteNonQuery(handler);
    }
}

//Síncrono
public int ExecuteCommand(string command)
{
    using (SqlCommand sqlCommand = new SqlCommand(command, sqlConnection))
    {
        return sqlCommand.ExecuteNonQuery();
    }
}

Comparando los tres métodos, podemos comprobar que la manera de escribirlo con async/await a partir de C# 5 es prácticamente igual que si lo hiciésemos de forma síncrona convencional, lo que lo hace más fácil de entender.

Llegados a este punto, te puedes estar preguntando de qué te sirve esto a ti, o dónde puedes usarlo. La respuesta es muy fácil, puedes (y debes) usarlo siempre que consumas un recurso externo a tu código (un fichero, una base de datos, un servicio online...). En general cualquier operación que pueda llegar a tardar por algún motivo ajeno a tu código y que por lo tanto pueda bloquear tu aplicación.

También puedes usarlo siempre que quieras ejecutar algo en segundo plano dejando disponibles los recursos mientras lo haces.

Puedes ver el efecto de usarlo o no usarlo, en el siguiente vídeo en el que se ve el efecto de usar o no usar asincronía en una interfaz de usuario WinForms:

Algo importante a tener en cuenta es que, para que nuestro código sea de verdad asíncrono, debemos utilizar métodos async en toda la cadena de acontecimientos que se desarrollen. Por ejemplo:

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace CampusMVP
{
    class Program
    {
        static void Main(string[] args)
        {
            SearchContentAsync().Wait();
        }

        static async Task SearchContentAsync()
        {
            string search = "CampusMVP";
            string path = "C://TestFolder";

            var filesWithContent = await FileSearcher.SearchInFilesAsync(path, search);

            Console.WriteLine($"Se ha encontrado {search} en {filesWithContent.Count} fichero/s.");
            foreach (var file in filesWithContent)
            {
                Console.WriteLine($"\t{file}");
            }
            Console.ReadLine();
        }
    }

    public static class FileSearcher
    {
        public static async Task<List<string>> SearchInFilesAsync(string path, string search)
        {
            var result = new List<string>();
            foreach (var file in Directory.GetFiles(path))
            {
                if (await ExistsContentInFileAsync(file, search))
                {
                    result.Add(file);
                }
            }
            return result;
        }

        private static async Task<bool> ExistsContentInFileAsync(string filePath, string search)
        {
            var content = await ReadFileAsync(filePath);
            return content.Contains(search);
        }

        private static async Task<string> ReadFileAsync(string filePath)
        {
            return await File.ReadAllTextAsync(filePath);
        }
    }
}

OJO: En el ejemplo utilizo static async Task Main, esto fue introducido en C# 7.1. En el caso de que te produzca un error, puedes cambiarlo por static void Main(string[] args) y cambiar await SearchContent(); por SearchContent().Wait();, para versiones anteriores del lenguaje.

En este ejemplo lo que se hace es buscar ficheros por su contenido en un directorio y de manera asíncrona. Como la lectura de ficheros es asíncrona, debemos utilizar Task y async/await hacia arriba en el árbol de llamadas, o nuestro código se ejecutaría de forma síncrona, es decir, bloqueando el hilo de ejecución si en alguno de los métodos de la pila de llamadas efectuamos una operación síncrona.

Una cosa a tener en cuenta es que no siempre se puede utilizar la palabra clave async (y por tanto tampoco await). Los métodos que la utilicen deben tener un tipo de retorno muy concreto. Los tipos de retorno que permite usar async son:

  • void
  • Task
  • Task<T>

Esto tiene lógica porque lo que devuelves realmente es una tarea (o nada en el caso de void, que se añadió para poder usarlo en los manejadores de eventos).

También conviene tener en cuenta que existe una convención de nombres de métodos a la hora de crear métodos asíncronos. Éstos tienen que terminar su nombre con el sufijo Async para poder diferenciarlos fácilmente (es decir, no es obligatorio, pero sí muy recomendable).

Con esto, vas a poder conseguir que no se bloquee una interfaz, que tu aplicación ASP.NET o ASP.NET Core sean capaces de responder a más peticiones (y por tanto escalar mejor), o simplemente lograrás optimizar tus recursos.

Espero haber conseguido que veas las ventajas de utilizar asincronismo 😊

Jorge Turrado Jorge lleva en el mundo de la programación desde los tiempos de .Net Framework 3.0. Es experto en la plataforma .NET, en Kubernetes y en técnicas de integración continua entre otras cosas. Actualmente trabaja como Staff SRE en la empresa SCRM Lidl International Hub. Microsoft lo ha reconocido como MVP en tecnologías de desarrollo, es CNCF Ambassador y maintainer oficial de KEDA, el autoescalador de Kubernetes basado en eventos. Puedes seguirlo en Twitter: @JorgeTurrado o en su blog FixedBuffer Ver todos los posts de Jorge Turrado
Archivado en: Lenguajes y plataformas

Boletín campusMVP.es

Solo cosas útiles. Una vez al mes.

🚀 Únete a miles de desarrolladores

DATE DE ALTA

x No me interesa | x Ya soy suscriptor

La mejor formación online para desarrolladores como tú

Comentarios (5) -

Muchisimas gracias por el articulo!

Responder

Buenas tardes,

muchas gracias por tu aporte,

yo utilizo el lenguaje Visual Basic .net, como seria el codigo en este lenguaje?

o

un ejemplo de como utilizar async?

gracias,

saludos,

Responder

Hola! Estupendo artículo! Precisamente lo he encontrado porque estoy tratando de convertir un código en asíncrono y estoy iniciándome con esto.

Me surge una duda existencial, si hacemos await para cada llamada una dentro de otra, ¿no estaríamos convirtiendo el proceso en síncrono? Si tenemos que esperar a que la llamada acabe para poder obtener el resultado y seguir, ¿no es lo mismo?

Esta duda me surge porque me pasa lo mismo. Tengo consultas a bases de datos y llamadas a objetos COM externos (no asíncronos) que tengo que llamar, pero la ejecución de una cosa depende del resultado de la anterior. Si a fin de cuentas tengo que esperar a que acabe, ¿no es lo mismo que si fuera síncrono?

Por ejemplo está el caso de una consulta a una base de datos. Creamos el objecto de conexión y le hacemos un Open, pero está el OpenAsync también, ok, pero para poder ejecutar la consulta con un por ejemplo ExecuteNonQueryAsync, tenemos que tener la conexión abierta, si no, da error. ¿De qué sirve hacerlo asíncrono, si al final cada cosa tiene que esperar por la anterior?

Sí que lo veo útil cuando se hacen llamadas a recursos externos que ejecutan procesos completos y que te puedes encargar de hacer cosas mientras tanto porque no dependen de su respuesta. Podrías llamar a un api para insertar un cliente, por ejemplo, mientras tanto ir ordenando una lista o creando un fichero o lo que sea y cuando necesites el resultado de la api, pones el await para tenerlo y continuar. Pero convertirlo todo a asíncrono es lo que no acabo de entender del todo su utilidad, que si sí que la tiene genial, pero querría saber si es correcto.

Gracias!

Marcos.

Responder

José Manuel Alarcón
José Manuel Alarcón

Hola Marcos:

Es una pregunta que suelen tener muchos principiantes porque todavía no tienen el concepto claro... Es importante comprender las diferencias entre las operaciones síncronas y asíncronas....

Al utilizar `await` en cada llamada dentro de otra, no estás convirtiendo todo el proceso en síncrono. Lo que estás permitiendo es que el hilo actual se libere y esté disponible para ejecutar otras tareas mientras se espera el resultado de la llamada asíncrona, que se ejecuta en paralelo en otro hilo. Cuando el resultado esté listo, el hilo se reanudará y continuará con la siguiente operación. El await te facilita esperar y no tener que complicarte la vida coordinando todos los hilos en segundo plano que se pudieran estar ejecutando.

La principal ventaja de las operaciones asíncronas es que permiten que tu programa sea más reactivo y eficiente en términos de uso de recursos. Mientras se espera el resultado de una operación asíncrona, el hilo puede realizar otras tareas en lugar de quedarse bloqueado, lo que permite aprovechar mejor los recursos del sistema.

Esto es especialmente útil en las aplicaciones web, ya que suele haber un número limitado (aunque alto) de hilos para poder procesar las peticiones que entran. Si usas código asíncrono, mientras se espera la respuesta de la base de datos o de cualquier operación asíncrona que realices, el hilo principal que atiende a la petición actual puede estar disponible para atender otras peticiones mientras tanto, lo que aumenta mucho la escalabilidad de tu aplicación web.

En el caso de las consultas a la base de datos, aunque algunas operaciones dependan de las anteriores, la ejecución asíncrona aún tiene sus beneficios. Por ejemplo, si tienes varias consultas que se pueden ejecutar en paralelo, puedes iniciarlas de manera asíncrona y luego usar `await Task.WhenAll(...)` para esperar a que todas finalicen. Esto te permite aprovechar el paralelismo y reducir el tiempo total de ejecución de todas las consultas que has lanzado.

En resumen, aunque haya ocasiones en las que las operaciones asíncronas parezcan depender unas de otras, aún puedes beneficiarte de su uso al aprovechar el tiempo de espera para realizar otras tareas y mejorar la eficiencia general de tu programa.

Saludos,

Responder

Marcos Garcia
Marcos Garcia

Muchísimas gracias por la respuesta. Llevo tiempo programando, pero nunca me había planteado hacerlo así. Nunca es tarde si la dicha es buena 🙂. Trataré de usar y crear métodos asíncronos siempre que sea posible.

Gracias!

Responder

Pingbacks and trackbacks (1)+

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.