Construir Pipelines Async con C# async/await y Task.Run
Construir pipelines async en C# usando async/await, Task.Run, Task.WhenAll, Task.WhenAny, CancellationTokenSource, Channels y Parallel.ForEachAsync para I/O y CPU concurrente.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Descripcion general
El modelo async/await de C# proporciona una maquina de estados basada en el compilador que transforma metodos async en callbacks. Task y Task<T> representan operaciones async. Aqui se explica como basics de async/await, Task.WhenAll para ejecucion paralela, Task.WhenAny para primero-en-completar, CancellationTokenSource para cancellation, System.Threading.Channels para producer-consumer y Parallel.ForEachAsync para I/O paralelo limitado.
Cuando Usar Esto
-
For alternatives, see Concurrent Patterns with Go Goroutines and Channels.
-
Operaciones I/O async (llamadas HTTP, queries a base de datos, I/O de archivos)
-
Llamadas a API concurrentes con agregacion de resultados
-
Operaciones de larga duracion con soporte de cancellation
-
Pipelines producer-consumer con backpressure
Prerrequisitos
- .NET 8+
- Paquete
System.Threading.Channels(incluido en .NET 8+)
Solucion
1. async/await Basico
using System.Diagnostics;
public class BasicAsync
{
public static async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetStringAsync(url);
return response;
}
public static async Task RunAsync()
{
var sw = Stopwatch.StartNew();
// Secuencial — 2s total
var data1 = await FetchDataAsync("https://httpbin.org/delay/1");
var data2 = await FetchDataAsync("https://httpbin.org/delay/1");
sw.Stop();
Console.WriteLine($"Sequential: {sw.ElapsedMilliseconds}ms");
}
}
2. Task.WhenAll — Ejecucion Paralela
using System.Diagnostics;
public class ParallelAsync
{
public static async Task<string> FetchDataAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url);
}
public static async Task RunWhenAllAsync()
{
var sw = Stopwatch.StartNew();
var urls = new[]
{
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1"
};
// Paralelo — ~1s total en lugar de 5s
var tasks = urls.Select(FetchDataAsync);
var results = await Task.WhenAll(tasks);
sw.Stop();
Console.WriteLine($"Parallel: {sw.ElapsedMilliseconds}ms");
Console.WriteLine($"Fetched {results.Length} responses");
}
}
3. Task.WhenAny — Primero en Completar
public class FirstResultAsync
{
public static async Task<string> FetchWithTimeoutAsync(string url, TimeSpan timeout)
{
using var client = new HttpClient();
var fetchTask = client.GetStringAsync(url);
var timeoutTask = Task.Delay(timeout);
// Retorna cuando cualquiera completa
var completedTask = await Task.WhenAny(fetchTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"Request to {url} timed out after {timeout}");
}
return await fetchTask;
}
public static async Task RunWhenAnyAsync()
{
try
{
var result = await FetchWithTimeoutAsync(
"https://httpbin.org/delay/5",
TimeSpan.FromSeconds(2)
);
Console.WriteLine(result);
}
catch (TimeoutException ex)
{
Console.WriteLine(ex.Message);
}
}
}
4. CancellationToken — Cancellation Cooperativa
using System.Threading;
public class CancellationAsync
{
public static async Task ProcessWithCancellationAsync(
CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100, cancellationToken);
Console.WriteLine($"Processing item {i}");
}
}
public static async Task RunCancellationAsync()
{
using var cts = new CancellationTokenSource();
// Cancelar despues de 500ms
cts.CancelAfter(TimeSpan.FromMilliseconds(500));
try
{
await ProcessWithCancellationAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was cancelled");
}
}
}
5. Task.Run para Trabajo CPU-Bound
public class CpuBoundAsync
{
public static long ComputeSum(long start, long end)
{
long sum = 0;
for (long i = start; i < end; i++)
{
sum += i;
}
return sum;
}
public static async Task RunCpuBoundAsync()
{
var sw = Stopwatch.StartNew();
// Ejecutar trabajo CPU-bound en thread pool — no bloquear la async state machine
var task1 = Task.Run(() => ComputeSum(0, 50_000_000));
var task2 = Task.Run(() => ComputeSum(50_000_000, 100_000_000));
var results = await Task.WhenAll(task1, task2);
sw.Stop();
Console.WriteLine($"Sum: {results[0] + results[1]}");
Console.WriteLine($"Time: {sw.ElapsedMilliseconds}ms");
}
}
6. System.Threading.Channels — Producer-Consumer
using System.Threading.Channels;
public class ChannelExample
{
public static async Task RunChannelAsync()
{
// Bounded channel con capacidad 10
var channel = Channel.CreateBounded<int>(10);
// Producer
async Task ProduceAsync()
{
for (int i = 0; i < 100; i++)
{
await channel.Writer.WriteAsync(i);
Console.WriteLine($"Produced: {i}");
}
channel.Writer.Complete();
}
// Consumer
async Task ConsumeAsync(int workerId)
{
await foreach (var item in channel.Reader.ReadAllAsync())
{
await Task.Delay(50); // Simular procesamiento
Console.WriteLine($"Worker {workerId} consumed: {item}");
}
}
// Iniciar 3 consumers y 1 producer
var consumers = Enumerable.Range(0, 3)
.Select(id => ConsumeAsync(id));
var producer = ProduceAsync();
await Task.WhenAll(
Task.WhenAll(consumers),
producer
);
Console.WriteLine("Channel processing complete");
}
}
7. Parallel.ForEachAsync — I/O Paralelo Limitado
public class ParallelForEachAsync
{
public static async Task RunParallelForEachAsync()
{
var urls = Enumerable.Range(1, 20)
.Select(i => $"https://httpbin.org/delay/1?page={i}")
.ToList();
using var client = new HttpClient();
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 5 // Max 5 concurrentes
};
int successCount = 0;
await Parallel.ForEachAsync(urls, options, async (url, ct) =>
{
try
{
var response = await client.GetStringAsync(url, ct);
Interlocked.Increment(ref successCount);
}
catch (Exception ex)
{
Console.WriteLine($"Failed {url}: {ex.Message}");
}
});
Console.WriteLine($"Successfully fetched {successCount}/{urls.Count}");
}
}
8. SemaphoreSlim — Rate Limiting Async
public class SemaphoreAsync
{
private static readonly HttpClient client = new();
public static async Task RunSemaphoreAsync()
{
var urls = Enumerable.Range(1, 50)
.Select(i => $"https://httpbin.org/delay/1?id={i}")
.ToList();
using var semaphore = new SemaphoreSlim(5); // Max 5 concurrentes
var sw = Stopwatch.StartNew();
var tasks = urls.Select(async url =>
{
await semaphore.WaitAsync();
try
{
var response = await client.GetStringAsync(url);
return response.Length;
}
finally
{
semaphore.Release();
}
});
var results = await Task.WhenAll(tasks);
sw.Stop();
Console.WriteLine($"Fetched {results.Length} URLs in {sw.ElapsedMilliseconds}ms");
}
}
9. IAsyncEnumerable — Async Streams
public class AsyncStreamExample
{
public static async IAsyncEnumerable<int> GenerateNumbersAsync(
int count,
[System.Runtime.CompilerServices.EnumeratorCancellation]
CancellationToken cancellationToken = default)
{
for (int i = 0; i < count; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100, cancellationToken);
yield return i;
}
}
public static async Task RunAsyncStreamAsync()
{
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
try
{
await foreach (var num in GenerateNumbersAsync(100, cts.Token))
{
Console.WriteLine($"Received: {num}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Stream cancelled");
}
}
}
Como Funciona
async/await: El compilador de C# transforma un metodoasyncen una maquina de estados. En cadaawait, el metodo se suspende y retorna control al caller. Cuando el task awaited completa, la continuacion se reanuda en un thread del thread pool (o el synchronization context si existe).Task.WhenAll: Crea un task que completa cuando todos los tasks de entrada completan. Si cualquier task lanza una excepcion,WhenAllrelanza la primera excepcion. Los tasks se ejecutan concurrentemente en el thread pool.Task.WhenAny: Retorna el primer task en completar (ya sea exito, fallo o cancelacion). Util para timeouts y racing de multiples fuentes.CancellationTokenSource: Crea cancellation tokens que se propagan a operaciones async.Cancel()dispara la cancellation.CancelAfterauto-cancela despues de un delay. Los metodos async verificanThrowIfCancellationRequested()o pasan el token a llamadas downstream.System.Threading.Channels: Una cola thread-safe para escenarios async producer-consumer.WriteAsyncbloquea cuando el channel esta lleno (bounded).ReadAllAsyncretorna unIAsyncEnumerableque produce items a medida que llegan.Parallel.ForEachAsync: Introducido en .NET 6. Ejecuta un delegate async para cada item con paralelismo limitado.MaxDegreeOfParallelismcontrola el limite de concurrencia.
Variantes
TaskCompletionSource — Bridge de Callbacks a Tasks
public static Task<string> FromEventAsync()
{
var tcs = new TaskCompletionSource<string>();
// Algun API basada en eventos
var timer = new System.Timers.Timer(1000);
timer.Elapsed += (s, e) =>
{
timer.Dispose();
tcs.SetResult("Timer fired");
};
timer.Start();
return tcs.Task;
}
// Uso
var result = await FromEventAsync();
ConfigureAwait(false)
// En codigo de libreria, usa ConfigureAwait(false) para evitar
// capturar el synchronization context (mejora rendimiento
// y previene deadlocks en apps UI)
public static async Task<string> FetchAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetAsync(url).ConfigureAwait(false);
return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
}
Inicializacion Lazy Async
public class AsyncLazy<T>
{
private readonly Lazy<Task<T>> _lazy;
public AsyncLazy(Func<Task<T>> factory)
{
_lazy = new Lazy<Task<T>>(() => Task.Run(factory));
}
public Task<T> Value => _lazy.Value;
}
// Uso
var lazyConfig = new AsyncLazy<string>(LoadConfigAsync);
var config = await lazyConfig.Value; // Carga una vez, cachea el resultado
Mejores Practicas
- Usar
async/awaitsobre.Resulty.Wait(): Bloquear en codigo async puede causar deadlocks, especialmente en aplicaciones con synchronization context (ASP.NET clasico, WPF, WinForms). - Usar
ConfigureAwait(false)en codigo de libreria: Previene capturar el synchronization context, mejorando rendimiento y evitando deadlocks. - Pasar
CancellationTokena traves de la cadena de llamadas: Cada metodo async deberia aceptar y propagar unCancellationToken. Verifica conThrowIfCancellationRequested()en loops. - Usar
Task.Runsolo para trabajo CPU-bound: No usesTask.Runpara metodos async I/O-bound — ya ceden el thread. Usalo para offload trabajo CPU al thread pool. - Preferir
Task.WhenAllsobre awaits secuenciales:await task1; await task2;se ejecuta secuencialmente.await Task.WhenAll(task1, task2)se ejecuta concurrentemente. - Usar
SemaphoreSlimpara rate limiting async:SemaphoreSlim.WaitAsynces compatible con async.Semaphore(la version sync) bloquea el thread.
Errores Comunes
- Usar
.Resulto.Wait(): Bloquea el thread y puede causar deadlock. Siempreawaitoperaciones async. - Olvidar
ConfigureAwait(false)en librerias: Captura el synchronization context innecesariamente, causando problemas de rendimiento y potenciales deadlocks. - No propagar
CancellationToken: Si aceptas un token pero no lo pasas a llamadas downstream, la cancellation no funcionara. - Usar
async void: Los metodosasync voidno pueden ser awaited y las excepciones crashean el proceso. Usaasync Tasken su lugar. La unica excepcion son event handlers. - Olvidar
Complete()en writers de channels: Si no llamasWriter.Complete(),ReadAllAsynccuelga para siempre esperando mas items.
FAQ
Cual es la diferencia entre Task y Task<T>?
Task representa una operacion async sin valor de retorno (como void). Task<T> representa una operacion async que retorna un valor de tipo T. Usa Task para operaciones fire-and-forget y Task<T> cuando necesitas el resultado.
Deberia usar Task.Run para metodos async I/O?
No. Los metodos async I/O ya ceden el thread mientras esperan. Task.Run solo agregaria un hop innecesario al thread pool. Usa Task.Run solo para trabajo CPU-bound que bloquearia la async state machine.
Que es ConfigureAwait(false) y cuando deberia usarlo?
ConfigureAwait(false) le dice al awaiter que no capture el synchronization context actual. Usalo en codigo de libreria para mejorar rendimiento y evitar deadlocks. En aplicaciones UI (WPF, WinForms), el synchronization context es el UI thread — capturarlo significa que las continuaciones se ejecutan en el UI thread, lo cual puede no ser deseable.
Como se comparan los channels con BlockingCollection?
System.Threading.Channels es compatible con async — WriteAsync y ReadAllAsync no bloquean threads. BlockingCollection bloquea threads, lo cual es desperdicio en aplicaciones async. Usa channels para patrones async producer-consumer.
Que es IAsyncEnumerable y como difiere de Task<IEnumerable>?
IAsyncEnumerable<T> produce items asincronicamente a medida que estan disponibles. Task<IEnumerable<T>> retorna todos los items a la vez cuando el task completa. Usa IAsyncEnumerable para streaming de datos (ej., filas de base de datos, eventos en tiempo real).
¿Esta solución está lista para producción?
Sí. Los ejemplos de código arriba muestran implementaciones probadas. Adapta el manejo de errores y la configuración a tu entorno específico antes de desplegar.
¿Cuáles son las características de rendimiento?
El rendimiento depende de tu volumen de datos e infraestructura. Las soluciones mostradas priorizan claridad. Para escenarios de alto throughput, añade caching, batching y connection pooling según sea necesario.
¿Cómo depuro problemas con este enfoque?
Empieza con el ejemplo mínimo de arriba. Añade logging en cada paso. Prueba con entradas pequeñas primero, luego escala. Usa el debugger de tu lenguaje para revisar los edge cases.
Recursos Relacionados
Compose Asynchronous Pipelines with Java CompletableFuture
Build non-blocking async pipelines in Java using CompletableFuture with thenCompose, thenCombine, allOf, anyOf, exception handling, timeouts, and custom thread pools.
RecipeConcurrent Async Tasks with asyncio.gather and Task Groups
Execute multiple async operations concurrently in Python using asyncio.gather, asyncio.TaskGroup, error handling with return_exceptions, timeouts, and semaphores for rate limiting.
GuideConcurrency Patterns Guide
A guide to common concurrency patterns and what works for writing safe, efficient concurrent code.
RecipeScale Concurrent Applications with Java Virtual Threads
Scale Java applications with virtual threads from Project Loom. Use Thread.ofVirtual, Executors.newVirtualThreadPerTaskExecutor, structured concurrency, and scoped values.
RecipeBuild Async Systems with Rust Tokio Runtime
Build async systems in Rust using the Tokio runtime with tasks, channels, select, synchronization primitives, graceful shutdown, and structured concurrency patterns.
RecipeConcurrent Patterns with Go Goroutines and Channels
Build concurrent systems in Go using goroutines, channels, select statements, worker pools, fan-out/fan-in, pipelines, context cancellation, and rate limiting with tickers.