El modelo async y await es la base de las aplicaciones modernas en .NET. Permite mantener la capacidad de respuesta sin invertir esfuerzo en gestionar hilos manualmente. Una vez asimilados los conceptos, el codigo asincrono se lee casi como codigo sincrono, con la gran diferencia de que un await no bloquea el hilo actual mientras espera un resultado externo.
Task y ValueTask como tipos de retorno
Un metodo async devuelve habitualmente Task o Task<T>. Task representa una operacion que se completara en el futuro sin valor de retorno, y Task<T> una que completara con un valor del tipo indicado. Es lo que permite al llamador aplicar await y reanudar cuando el resultado esta disponible.
public async Task GuardarAsync(Pedido pedido)
{
await _repositorio.InsertarAsync(pedido);
await _bus.PublicarAsync(new PedidoCreado(pedido.Id));
}
public async Task<Pedido> LeerAsync(int id)
{
var entidad = await _repositorio.BuscarAsync(id);
return Mapper.Map(entidad);
}
El tipo ValueTask<T> es una alternativa mas ligera, util cuando una operacion puede completar de forma sincrona en la mayoria de los casos. Evita la asignacion de un Task y reduce el coste de rutas calientes.
public ValueTask<string?> LeerDeCacheAsync(string clave)
{
if (_cache.TryGetValue(clave, out var valor))
{
return ValueTask.FromResult<string?>(valor);
}
return LeerAsincronoAsync(clave);
}
private async ValueTask<string?> LeerAsincronoAsync(string clave) =>
await _db.LeerAsync(clave);
El camino rapido no genera ningun Task, mientras que el camino lento sigue el flujo asincrono habitual. Esta tecnica es valiosa en librerias que exponen metodos llamados miles de veces por segundo. Para aplicaciones cotidianas, Task y Task<T> son la eleccion por defecto.
Un
async voidsolo se justifica en manejadores de eventos. Las excepciones lanzadas en un metodoasync voidpueden terminar el proceso, ya que nadie las recibe a traves de unTask.
Ejecutar tareas en paralelo
Cuando varias operaciones asincronas son independientes, esperarlas secuencialmente con un await tras otro desaprovecha tiempo. La respuesta es iniciar todas y esperarlas en bloque con Task.WhenAll, que devuelve un Task que completa cuando todas han finalizado.
public async Task<DashboardViewModel> CargarDashboardAsync(int usuarioId)
{
Task<Usuario> usuarioTask = _usuarios.BuscarAsync(usuarioId);
Task<IReadOnlyList<Pedido>> pedidosTask = _pedidos.UltimosAsync(usuarioId);
Task<IReadOnlyList<Mensaje>> mensajesTask = _mensajes.BandejaAsync(usuarioId);
await Task.WhenAll(usuarioTask, pedidosTask, mensajesTask);
return new DashboardViewModel(
usuarioTask.Result,
pedidosTask.Result,
mensajesTask.Result);
}
Las tres peticiones se lanzan al mismo tiempo. El tiempo total de espera se aproxima al de la operacion mas lenta, no a la suma de las tres. Cuando Task.WhenAll detecta una excepcion, todas las excepciones se agregan en una AggregateException, pero al hacer await solo se propaga la primera para facilitar la lectura.
Para procesar una coleccion de elementos en paralelo con un grado de concurrencia controlado, Parallel.ForEachAsync es la opcion idiomatica.
public async Task ActualizarTodosAsync(IReadOnlyList<int> ids, CancellationToken ct)
{
await Parallel.ForEachAsync(
ids,
new ParallelOptions
{
MaxDegreeOfParallelism = 8,
CancellationToken = ct
},
async (id, token) =>
{
await _repositorio.ActualizarAsync(id, token);
});
}
El parametro MaxDegreeOfParallelism acota el numero de operaciones simultaneas, protegiendo recursos externos. Sin limite, un bucle sobre miles de elementos abriria miles de conexiones a la vez, algo que casi ningun backend toleraria.
flowchart LR
A[Coleccion] --> B[Parallel.ForEachAsync]
B --> C{Concurrencia N}
C --> D[Worker 1]
C --> E[Worker 2]
C --> F[Worker N]
D --> G[Operacion remota]
E --> G
F --> G
Cancelacion con CancellationToken
La cancelacion en .NET es cooperativa. Ningun codigo termina a otro de forma forzosa. La operacion que puede ser cancelada debe aceptar un CancellationToken y respetarlo en sus puntos de suspension.
public async Task ProcesarArchivoAsync(string ruta, CancellationToken ct)
{
using var fs = File.OpenRead(ruta);
var buffer = new byte[4096];
while (true)
{
ct.ThrowIfCancellationRequested();
int leidos = await fs.ReadAsync(buffer, ct);
if (leidos == 0) break;
await ProcesarChunkAsync(buffer.AsMemory(0, leidos), ct);
}
}
El token se pasa a cada operacion asincrona. Si el consumidor invoca Cancel, las operaciones lanzan OperationCanceledException y el metodo finaliza de forma controlada. La llamada explicita a ThrowIfCancellationRequested garantiza respuesta en los puntos donde no hay await.
La fuente de tokens es CancellationTokenSource, que tambien permite combinar tokens y cancelar por tiempo.
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(30));
try
{
await ProcesarArchivoAsync("datos.csv", cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operacion cancelada por tiempo.");
}
La variante CreateLinkedTokenSource une varios tokens en uno solo, muy util cuando se quiere combinar un token externo con un timeout interno.
public async Task<int> DescargarAsync(Uri uri, CancellationToken ct)
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(TimeSpan.FromSeconds(5));
return await _http.GetIntAsync(uri, timeout.Token);
}
Documentar en la firma que un metodo acepta un
CancellationTokenes parte del contrato publico. Quien lo invoca entiende que la operacion es cancelable y planifica el flujo en consecuencia.
Flujos asincronos con IAsyncEnumerable
Cuando el resultado de una operacion es una secuencia que se produce poco a poco, como paginas de una API o registros de un streaming, la interfaz ideal es IAsyncEnumerable<T>. Permite iterar con await foreach y libera cada elemento en cuanto esta disponible.
public async IAsyncEnumerable<Factura> LeerFacturasAsync(
int desdeId,
[EnumeratorCancellation] CancellationToken ct = default)
{
int ultimo = desdeId;
while (true)
{
ct.ThrowIfCancellationRequested();
var lote = await _api.ObtenerLoteAsync(ultimo, ct);
if (lote.Count == 0) yield break;
foreach (var f in lote)
{
yield return f;
ultimo = f.Id;
}
}
}
El consumidor itera con await foreach, recibiendo cada Factura cuando esta lista sin esperar a que la secuencia completa termine.
await foreach (var f in LeerFacturasAsync(0, cts.Token))
{
await _procesador.RegistrarAsync(f);
}
El atributo EnumeratorCancellation asegura que el token pasado con WithCancellation llega correctamente al cuerpo del generador. IAsyncEnumerable reemplaza patrones antiguos basados en Task<List<T>> cuando el tamano total de la secuencia es grande o desconocido.
Buenas practicas de async
Hay cuatro reglas que evitan la mayoria de problemas. La primera es propagar async hasta el punto de entrada de la operacion. Mezclar Result o Wait en medio de una cadena asincrona bloquea hilos y anula la ventaja del modelo.
public int ObtenerCantidad() => _api.ObtenerCantidadAsync().Result;
Esta linea parece inocua pero puede causar un deadlock en contextos de sincronizacion antiguos y siempre bloquea un hilo del pool. La version correcta es hacer el metodo asincrono y propagar el await hasta donde sea necesario.
La segunda es pasar siempre un CancellationToken en metodos publicos. Esto permite al consumidor combinar cancelaciones y aplicar timeouts propios sin alterar la logica.
La tercera es evitar async void salvo en manejadores de eventos. Cuando se quiere ejecutar una operacion asincrona desde un punto sincrono, se puede envolver en Task.Run si de verdad hace falta, aunque es una solucion de emergencia.
La cuarta es configurar ConfigureAwait(false) en librerias que no necesiten retomar el contexto original de sincronizacion. En codigo de aplicacion moderno, como ASP.NET Core, no suele hacer falta, ya que no hay contexto que capturar.
public async Task<string> LeerRecursoAsync(string ruta)
{
using var http = new HttpClient();
return await http.GetStringAsync(ruta).ConfigureAwait(false);
}
Diagnostico de codigo asincrono
Los problemas tipicos se detectan con analizadores estaticos y con herramientas de diagnostico integradas en el IDE. Los avisos mas utiles a configurar son los siguientes.
El aviso por async sin await detecta metodos marcados como asincronos que no esperan ninguna tarea, sintoma de haber olvidado un await o de no necesitar el modificador.
El aviso por Task sin await alerta cuando un metodo devuelve un Task que nadie observa, lo que provoca excepciones silenciosas y orden de ejecucion incorrecto.
El aviso por CancellationToken no propagado avisa cuando una operacion interna podria cancelarse pero se le pasa default en lugar del token recibido.
public async Task ProcesarAsync(CancellationToken ct)
{
await _repositorio.GuardarAsync(ct);
await _notificador.AvisarAsync();
}
El analizador senalaria la llamada a AvisarAsync porque no recibe el token. Cuando una operacion dura minutos, perder la cancelacion es un defecto tangible en producccion.
Siguiendo estas pautas, el codigo asincrono en C# se mantiene predecible incluso cuando la logica involucra cientos de operaciones concurrentes contra servicios externos.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, C# es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de C#
Explora más contenido relacionado con C# y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Escribir codigo asincrono correcto con async await, evitar bloqueos con Task y ValueTask, coordinar tareas concurrentes y cancelar operaciones largas con CancellationToken.