Introducción: ¿por qué se necesitan colas de tareas en C#?
\nEn el desarrollo moderno con C# (.NET), a menudo surge la necesidad de organizar el procesamiento asíncrono de datos. Un ejemplo clásico es un servidor web que recibe solicitudes y debe procesarlas en segundo plano, sin bloquear el hilo principal. O una aplicación que descarga archivos de internet y procesa en paralelo los ya descargados. Para estos escenarios, el patrón «Productor-Consumidor» es ideal, y su base es una cola de tareas.
\nLa cola de tareas permite transferir trabajo de manera segura de un hilo (u operación asíncrona) a otro, sincronizando el acceso a datos compartidos. En este artículo, veremos tres formas principales de implementar colas en C#: ConcurrentQueue, BlockingCollection y el moderno System.Threading.Channels. Cada uno es adecuado para diferentes escenarios, y los analizaremos con ejemplos prácticos.
1. ConcurrentQueue — cola básica segura para subprocesos
\nConcurrentQueue<T> es la cola segura para subprocesos más simple del espacio de nombres System.Collections.Concurrent. Utiliza algoritmos libres de bloqueo (comparación e intercambio, CAS) para garantizar un alto rendimiento. Sin embargo, no bloquea el hilo cuando la cola está vacía; deberá implementar la espera por su cuenta.
Ejemplo: carga de datos desde múltiples fuentes
\nSupongamos que tenemos una tarea: descargar 10 páginas web y procesarlas. Un hilo (Productor) agrega URL a la cola, y varios hilos (Consumidores) las toman y descargan.
\nusing System;\nusing System.Collections.Concurrent;\nusing System.Threading.Tasks;\n\npublic class ConcurrentQueueExample\n{\n public static async Task RunAsync()\n {\n var queue = new ConcurrentQueue<string>();\n var urls = new[] { "https://example.com", "https://dotnet.microsoft.com" };\n\n // Productor: agregamos tareas\n foreach (var url in urls)\n {\n queue.Enqueue(url);\n }\n\n // Consumidores: procesamos tareas\n var tasks = new Task[3];\n for (int i = 0; i < tasks.Length; i++)\n {\n tasks[i] = Task.Run(() =>\n {\n while (queue.TryDequeue(out var url))\n {\n Console.WriteLine($"Procesando: {url}");\n // Simulación de trabajo\n Task.Delay(500).Wait();\n }\n });\n }\n\n await Task.WhenAll(tasks);\n Console.WriteLine("Todas las tareas completadas.");\n }\n}\nVentajas: simplicidad, alto rendimiento con gran cantidad de operaciones.
\nDesventajas: no tiene mecanismo de espera incorporado (se necesita un bucle con verificación), difícil limitar el tamaño de la cola.
2. BlockingCollection — cola con bloqueo y límite
\nBlockingCollection<T> es un envoltorio sobre una colección segura para subprocesos que agrega la capacidad de bloquear el hilo consumidor mientras la cola esté vacía, y el hilo productor mientras la cola esté llena. Por defecto, utiliza ConcurrentQueue, pero se puede usar cualquier colección que implemente IProducerConsumerCollection<T>.
Ejemplo: procesamiento de pedidos con límite de búfer
\nImagine un sistema de procesamiento de pedidos. El productor genera pedidos y tres consumidores los procesan. El búfer está limitado a 10 elementos para no consumir demasiada memoria.
\nusing System;\nusing System.Collections.Concurrent;\nusing System.Threading.Tasks;\n\npublic class BlockingCollectionExample\n{\n public static async Task RunAsync()\n {\n using var orders = new BlockingCollection<string>(boundedCapacity: 10);\n\n // Productor\n var producer = Task.Run(() =>\n {\n for (int i = 1; i <= 20; i++)\n