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, await y Task
  • 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: Task representa 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 CancellationToken permite 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)