Módulo 17: Programación asíncrona con Async/Await
Objetivos del módulo
- Entender qué es la programación asíncrona y por qué importa
- Dominar
async,awaityTask - Ejecutar tareas en paralelo
- Aplicar patrones asíncronos comunes
1. ¿Qué es la programación asíncrona?
📘 Concepto: La programación síncrona ejecuta las tareas una tras otra: si una tarda mucho (leer un archivo, llamar a una API), el programa se bloquea esperando. La programación asíncrona permite que el programa siga trabajando mientras espera.
Analogía
- Síncrono: Haces cola en un restaurante, no puedes hacer nada hasta que te sirvan.
- Asíncrono: Pides por app, mientras esperas lees un libro. Te avisan cuando está listo.
// SÍNCRONO: el programa se congela 3 segundos
static void DescargarDatos()
{
Thread.Sleep(3000); // Simula operación lenta
Console.WriteLine("Datos descargados");
}
// ASÍNCRONO: el programa sigue funcionando
static async Task DescargarDatosAsync()
{
await Task.Delay(3000); // Simula operación lenta SIN bloquear
Console.WriteLine("Datos descargados");
}
2. Task y Task<T>
📘 Concepto:
Taskrepresenta una operación en curso.Task<T>es lo mismo pero devuelve un resultado de tipo T.
// Task sin resultado (como void pero asíncrono)
static async Task SaludarAsync()
{
await Task.Delay(1000);
Console.WriteLine("¡Hola!");
}
// Task con resultado
static async Task<int> CalcularAsync(int a, int b)
{
await Task.Delay(500); // Simula trabajo
return a + b;
}
// Uso
await SaludarAsync();
int resultado = await CalcularAsync(5, 3);
Console.WriteLine($"Resultado: {resultado}"); // 8
| Concepto | Significado |
|---|---|
async | Marca un método como asíncrono |
await | “Espera” a que la tarea termine (sin bloquear) |
Task | Operación asíncrona sin resultado |
Task<T> | Operación asíncrona con resultado de tipo T |
3. Ejemplo real: llamadas HTTP
using System.Net.Http;
using System.Text.Json;
static async Task<string> ObtenerDatosWebAsync(string url)
{
using HttpClient client = new();
string respuesta = await client.GetStringAsync(url);
return respuesta;
}
// Uso
string datos = await ObtenerDatosWebAsync("https://jsonplaceholder.typicode.com/todos/1");
Console.WriteLine(datos);
// Deserializar la respuesta JSON
record Todo(int UserId, int Id, string Title, bool Completed);
static async Task<Todo?> ObtenerTodoAsync(int id)
{
using HttpClient client = new();
string json = await client.GetStringAsync(
$"https://jsonplaceholder.typicode.com/todos/{id}");
return JsonSerializer.Deserialize<Todo>(json, new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
});
}
Todo? todo = await ObtenerTodoAsync(1);
Console.WriteLine($"Tarea: {todo?.Title} (Completada: {todo?.Completed})");
4. Ejecutar tareas en paralelo
Task.WhenAll: esperar a que TODAS terminen
static async Task<string> DescargarPaginaAsync(string nombre, int ms)
{
Console.WriteLine($"Descargando {nombre}...");
await Task.Delay(ms);
Console.WriteLine($" ✓ {nombre} descargada ({ms}ms)");
return $"Contenido de {nombre}";
}
// SECUENCIAL: una tras otra (lento)
var sw = System.Diagnostics.Stopwatch.StartNew();
string p1 = await DescargarPaginaAsync("Google", 2000);
string p2 = await DescargarPaginaAsync("GitHub", 1500);
string p3 = await DescargarPaginaAsync("Stack Overflow", 1000);
Console.WriteLine($"Secuencial: {sw.ElapsedMilliseconds}ms"); // ~4500ms
// PARALELO: todas a la vez (rápido)
sw.Restart();
Task<string> t1 = DescargarPaginaAsync("Google", 2000);
Task<string> t2 = DescargarPaginaAsync("GitHub", 1500);
Task<string> t3 = DescargarPaginaAsync("Stack Overflow", 1000);
string[] resultados = await Task.WhenAll(t1, t2, t3); // Espera a las 3
Console.WriteLine($"Paralelo: {sw.ElapsedMilliseconds}ms"); // ~2000ms
foreach (string r in resultados)
Console.WriteLine(r);
Task.WhenAny: esperar a la PRIMERA que termine
static async Task<string> BuscarEnServidorAsync(string servidor, int ms)
{
await Task.Delay(ms);
return $"Respuesta de {servidor}";
}
// La más rápida gana
Task<string> s1 = BuscarEnServidorAsync("Servidor A", 3000);
Task<string> s2 = BuscarEnServidorAsync("Servidor B", 1000);
Task<string> s3 = BuscarEnServidorAsync("Servidor C", 2000);
Task<string> ganadora = await Task.WhenAny(s1, s2, s3);
string resultado = await ganadora;
Console.WriteLine(resultado); // "Respuesta de Servidor B" (1000ms, la más rápida)
5. Manejo de errores asíncronos
static async Task<string> OperacionRiesgosaAsync()
{
await Task.Delay(500);
throw new InvalidOperationException("Algo salió mal");
}
// try-catch funciona normalmente con await
try
{
string resultado = await OperacionRiesgosaAsync();
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Error capturado: {ex.Message}");
}
// Con Task.WhenAll, capturar errores de múltiples tareas
try
{
await Task.WhenAll(
OperacionRiesgosaAsync(),
OperacionRiesgosaAsync()
);
}
catch (Exception ex)
{
Console.WriteLine($"Al menos una tarea falló: {ex.Message}");
}
6. Cancelación con CancellationToken
📘 Concepto: Un
CancellationTokenpermite cancelar una operación asíncrona de forma limpia, sin dejarla corriendo en segundo plano.
static async Task OperacionLargaAsync(CancellationToken token)
{
for (int i = 1; i <= 10; i++)
{
// Comprobar si se pidió cancelar
token.ThrowIfCancellationRequested();
Console.WriteLine($"Paso {i}/10...");
await Task.Delay(1000, token);
}
Console.WriteLine("¡Operación completada!");
}
// Crear fuente de cancelación con timeout de 3 segundos
using CancellationTokenSource cts = new(TimeSpan.FromSeconds(3));
try
{
await OperacionLargaAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operación cancelada (timeout de 3s)");
}
7. Lectura/escritura de archivos asíncrona
// Escribir archivo de forma asíncrona
await File.WriteAllTextAsync("datos.txt", "Contenido del archivo");
// Leer archivo de forma asíncrona
string contenido = await File.ReadAllTextAsync("datos.txt");
Console.WriteLine(contenido);
// Leer líneas de forma asíncrona
string[] lineas = await File.ReadAllLinesAsync("datos.txt");
// Añadir texto
await File.AppendAllTextAsync("datos.txt", "\nMás contenido");
8. Patrones asíncronos comunes
Patrón retry (reintentos)
static async Task<T> ConReintentosAsync<T>(
Func<Task<T>> operacion,
int maxIntentos = 3,
int delayMs = 1000)
{
for (int intento = 1; intento <= maxIntentos; intento++)
{
try
{
return await operacion();
}
catch (Exception ex) when (intento < maxIntentos)
{
Console.WriteLine($"Intento {intento} falló: {ex.Message}. Reintentando...");
await Task.Delay(delayMs * intento); // Backoff exponencial
}
}
return await operacion(); // Último intento: deja propagar la excepción
}
// Uso
string resultado = await ConReintentosAsync(async () =>
{
using HttpClient client = new();
return await client.GetStringAsync("https://api.ejemplo.com/datos");
});
Patrón semáforo (limitar concurrencia)
static async Task ProcesarEnParaleloAsync(List<string> urls, int maxConcurrente = 3)
{
SemaphoreSlim semaforo = new(maxConcurrente);
using HttpClient client = new();
var tareas = urls.Select(async url =>
{
await semaforo.WaitAsync(); // Espera si hay muchas tareas activas
try
{
string resultado = await client.GetStringAsync(url);
Console.WriteLine($"Descargado: {url} ({resultado.Length} chars)");
}
finally
{
semaforo.Release(); // Libera el semáforo
}
});
await Task.WhenAll(tareas);
}
9. Ejercicios
Ejercicio 1: Descargador de URLs
Crea un programa que reciba una lista de URLs y las descargue todas en paralelo con HttpClient y Task.WhenAll. Muestra el tamaño de cada respuesta y el tiempo total.
Ejercicio 2: Temporizador con cancelación
Crea un temporizador que cuente de 1 a N segundos de forma asíncrona. El usuario puede cancelarlo escribiendo “stop”. Usa CancellationToken.
Ejercicio 3: Procesador de archivos
Lee todos los archivos .txt de una carpeta de forma asíncrona y en paralelo. Para cada uno, cuenta palabras, líneas y caracteres. Muestra un resumen final con totales.
Ejercicio 4: Monitor de APIs
Crea un monitor que haga ping a varias APIs cada 5 segundos (en paralelo) y registre: tiempo de respuesta, estado (OK/Error), timestamp. Muestra estadísticas cada minuto.
Resumen
| Concepto | Sintaxis |
|---|---|
| Método asíncrono | async Task Metodo() |
| Con resultado | async Task<int> Metodo() |
| Esperar resultado | int r = await MetodoAsync(); |
| Ejecutar en paralelo | await Task.WhenAll(t1, t2, t3); |
| Esperar al primero | await Task.WhenAny(t1, t2, t3); |
| Cancelar | CancellationTokenSource + CancellationToken |
| Archivo async | await File.ReadAllTextAsync("f.txt") |
| HTTP async | await client.GetStringAsync(url) |
| Delay sin bloquear | await Task.Delay(1000) |