En esta página
Programación asíncrona con async/await en C#
¿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 completarEn 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
- 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 conTask.WhenAll. - CancellationToken: Implementa
DescargarConTimeoutAsync(string url, int segundos)que cancele la operación si supera el tiempo límite. - Async stream: Crea un generador
GenerarFibonacciAsync()que devuelva números Fibonacci conIAsyncEnumerable<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.
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();
Inicia sesión para guardar tu progreso