En esta página

Programación asíncrona con async/await en C#

14 min lectura TextoCap. 3 — C# moderno

¿Por qué programación asíncrona?

Las operaciones de I/O (red, disco, base de datos) son lentas comparadas con la CPU. Sin async, el hilo queda bloqueado esperando la respuesta, desperdiciando recursos:

Síncrono:  Hilo → [ESPERA 500ms] → Procesa resultado
Asíncrono: Hilo → Inicia I/O → hace otra cosa → Reanuda al completar

En ASP.NET Core, un servidor con hilos bloqueados puede manejar pocas solicitudes concurrentes. Con async/await, el mismo servidor puede atender miles.

Task y Task\

Task representa una operación asíncrona. Task<T> representa una operación asíncrona que devuelve un valor:

// Task — operación que no devuelve valor
Task tarea = Task.Delay(1000);
await tarea;

// Task<T> — operación que devuelve un valor
Task<int> tareaConResultado = Task.Run(() => 42);
int resultado = await tareaConResultado;

// Crear tareas completadas (útil en tests y mocks)
Task        ya  = Task.CompletedTask;
Task<int>   val = Task.FromResult(42);
Task<string> err = Task.FromException<string>(new Exception("Error"));

async / await

async marca un método como asíncrono. await suspende la ejecución sin bloquear el hilo:

// Sin async — síncrono, bloquea el hilo
static string LeerArchivoSync(string ruta)
{
    return File.ReadAllText(ruta); // bloquea el hilo hasta terminar
}

// Con async — asíncrono, libera el hilo mientras espera
static async Task<string> LeerArchivoAsync(string ruta)
{
    return await File.ReadAllTextAsync(ruta); // libera el hilo
}

// Awaiting múltiple — secuencial (más lento)
static async Task SecuencialAsync()
{
    var r1 = await ObtenerDatos1Async(); // espera R1
    var r2 = await ObtenerDatos2Async(); // espera R2 — total = T1 + T2
    return (r1, r2);
}

// Awaiting múltiple — paralelo (más rápido)
static async Task ParaleloAsync()
{
    var t1 = ObtenerDatos1Async(); // inicia T1 sin await
    var t2 = ObtenerDatos2Async(); // inicia T2 sin await
    var (r1, r2) = await Task.WhenAll(t1, t2); // espera ambas — total = max(T1, T2)
    return (r1, r2);
}

Task.WhenAll y Task.WhenAny

// Task.WhenAll — espera que TODAS completen
static async Task EjemploConcurrenteAsync()
{
    var urls = new[]
    {
        "https://api.github.com/users/dotnet",
        "https://api.github.com/users/microsoft",
        "https://api.github.com/users/google",
    };

    using var http = new HttpClient();
    http.DefaultRequestHeaders.Add("User-Agent", "DotNetApp");

    // Inicia todas las solicitudes de forma concurrente
    var tareas = urls.Select(url => http.GetStringAsync(url)).ToList();
    string[] respuestas = await Task.WhenAll(tareas);

    for (int i = 0; i < respuestas.Length; i++)
        Console.WriteLine($"URL {i + 1}: {respuestas[i].Length} bytes");
}

// Task.WhenAny — toma el PRIMERO que complete
static async Task<string> PrimerResultadoAsync(IEnumerable<Task<string>> tareas)
{
    Task<string> primera = await Task.WhenAny(tareas);
    return await primera; // des-envolver para obtener el resultado o relanzar excepción
}

Manejo de errores asíncronos

// try-catch con await funciona igual que síncrono
static async Task ProcesarAsync()
{
    try
    {
        var datos = await ObtenerDatosAsync("https://api.ejemplo.com/recurso");
        Console.WriteLine($"Datos: {datos.Length} bytes");
    }
    catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
    {
        Console.WriteLine("Recurso no encontrado (404)");
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("Solicitud cancelada o timeout");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error inesperado: {ex.Message}");
    }
    finally
    {
        Console.WriteLine("Limpieza ejecutada siempre");
    }
}

// AggregateException con WhenAll
static async Task ManejarErroresWhenAllAsync()
{
    var tareas = new[]
    {
        Task.FromException<string>(new Exception("Error tarea 1")),
        Task.FromResult("OK"),
        Task.FromException<string>(new Exception("Error tarea 3")),
    };

    try
    {
        await Task.WhenAll(tareas);
    }
    catch
    {
        // Inspeccionar todos los errores
        foreach (var tarea in tareas.Where(t => t.IsFaulted))
            Console.WriteLine($"Error: {tarea.Exception?.InnerException?.Message}");
    }
}

CancellationToken

Mecanismo estándar para cancelar operaciones asíncronas:

// Patrón estándar para métodos cancelables
static async Task<List<Articulo>> ObtenerArticulosAsync(
    string categoria,
    CancellationToken ct = default) // siempre con default
{
    using var http = new HttpClient();
    var url = $"https://api.ejemplo.com/articulos?categoria={categoria}";

    // Pasar el token a operaciones anidadas
    var response = await http.GetAsync(url, ct);
    response.EnsureSuccessStatusCode();

    var json = await response.Content.ReadAsStringAsync(ct);
    return JsonSerializer.Deserialize<List<Articulo>>(json) ?? new();
}

// Cancelar manualmente
using var cts = new CancellationTokenSource();

// Configurar cancelación automática por tiempo
cts.CancelAfter(TimeSpan.FromSeconds(10));

// O cancelar desde otro hilo
Task.Run(() =>
{
    Thread.Sleep(3000);
    cts.Cancel();
    Console.WriteLine("Cancelación solicitada");
});

try
{
    var articulos = await ObtenerArticulosAsync("tecnologia", cts.Token);
    Console.WriteLine($"Artículos: {articulos.Count}");
}
catch (OperationCanceledException)
{
    Console.WriteLine("Operación cancelada");
}

Async Streams (IAsyncEnumerable\)

Para procesar secuencias de datos asíncronas elemento a elemento:

// Productor de stream asíncrono
static async IAsyncEnumerable<Lectura> LeerSensoresAsync(
    string dispositivoId,
    [System.Runtime.CompilerServices.EnumeratorCancellation]
    CancellationToken ct = default)
{
    while (!ct.IsCancellationRequested)
    {
        await Task.Delay(1000, ct); // simula lectura de sensor
        yield return new Lectura(
            DispositivoId: dispositivoId,
            Valor: Random.Shared.NextDouble() * 100,
            Timestamp: DateTime.UtcNow
        );
    }
}

record Lectura(string DispositivoId, double Valor, DateTime Timestamp);

// Consumidor
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await foreach (Lectura lectura in LeerSensoresAsync("sensor-01", cts.Token))
{
    Console.WriteLine($"[{lectura.Timestamp:HH:mm:ss}] Valor: {lectura.Valor:F2}");
}

Práctica

  1. Paralelo vs secuencial: Crea un método que simule 5 llamadas HTTP con Task.Delay(1000). Mide el tiempo de ejecución secuencial vs paralelo con Task.WhenAll.
  2. CancellationToken: Implementa DescargarConTimeoutAsync(string url, int segundos) que cancele la operación si supera el tiempo límite.
  3. Async stream: Crea un generador GenerarFibonacciAsync() que devuelva números Fibonacci con IAsyncEnumerable<long>, con un delay de 100ms entre cada número.

En la siguiente lección exploraremos records y pattern matching: las características más modernas de C# para modelar datos inmutables y expresar lógica condicional.

Task.WhenAll para paralelismo real
await tarea1; await tarea2; ejecuta las tareas EN SECUENCIA. Para ejecutarlas en paralelo, primero inicia ambas (sin await) y luego usa await Task.WhenAll(tarea1, tarea2). El paralelismo puede reducir el tiempo total de forma dramática cuando las operaciones son independientes.
ConfigureAwait en librerías
En librerías de clases, usa .ConfigureAwait(false) después de cada await para evitar capturar el contexto de sincronización. En aplicaciones (ASP.NET Core, WPF) no es necesario porque el runtime maneja el contexto automáticamente.
No uses async void
async void solo es aceptable en event handlers de UI (como Click). Fuera de esos casos, siempre devuelve Task. Los métodos async void no pueden ser awaited, los errores que lanzan no pueden capturarse fácilmente y pueden crashear la aplicación.
using System.Net.Http;
using System.Text.Json;

// Método asíncrono — siempre retorna Task o Task<T>
static async Task<string> ObtenerDatosAsync(string url)
{
    using var http = new HttpClient();
    // await no bloquea el hilo — cede control y reanuda al completar
    string json = await http.GetStringAsync(url);
    return json;
}

// Llamar múltiples tareas en PARALELO con WhenAll
static async Task EjecutarParaleloAsync()
{
    var urls = new[]
    {
        "https://jsonplaceholder.typicode.com/posts/1",
        "https://jsonplaceholder.typicode.com/posts/2",
        "https://jsonplaceholder.typicode.com/posts/3",
    };

    // Iniciar todas las tareas a la vez (NO con await aquí)
    Task<string>[] tareas = urls.Select(u => ObtenerDatosAsync(u)).ToArray();

    // Esperar a que TODAS completen
    string[] resultados = await Task.WhenAll(tareas);

    Console.WriteLine($"Recibidas {resultados.Length} respuestas");
}

// Punto de entrada async
await EjecutarParaleloAsync();