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:
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 😊