Introdução: por que usar filas de tarefas em C#?
\nNo desenvolvimento moderno em C# (.NET), frequentemente surge a necessidade de organizar o processamento assíncrono de dados. Um exemplo clássico é um servidor web que recebe requisições e precisa processá-las em segundo plano, sem bloquear a thread principal. Ou uma aplicação que baixa arquivos da internet e processa paralelamente os já baixados. Para esses cenários, o padrão «Producer-Consumer» (produtor-consumidor) é ideal, e sua base é uma fila de tarefas.
\nA fila de tarefas permite transferir trabalho com segurança de uma thread (ou operação assíncrona) para outra, sincronizando o acesso a dados compartilhados. Neste artigo, veremos três formas principais de implementar filas em C#: ConcurrentQueue, BlockingCollection e o moderno System.Threading.Channels. Cada uma é adequada para cenários diferentes, e as analisaremos com exemplos práticos.
1. ConcurrentQueue — fila thread-safe básica
\nConcurrentQueue<T> é a fila thread-safe mais simples do namespace System.Collections.Concurrent. Ela usa algoritmos lock-free (comparação e troca, CAS) para garantir alta performance. No entanto, ela não bloqueia a thread quando a fila está vazia — você precisará implementar a espera manualmente.
Exemplo: carregando dados de múltiplas fontes
\nSuponha que temos uma tarefa: baixar 10 páginas web e processá-las. Uma thread (Producer) adiciona URLs à fila, e várias threads (Consumers) as retiram e baixam.
\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 // Producer: adicionando tarefas\n foreach (var url in urls)\n {\n queue.Enqueue(url);\n }\n\n // Consumers: processando tarefas\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($"Processando: {url}");\n // Simulação de trabalho\n Task.Delay(500).Wait();\n }\n });\n }\n\n await Task.WhenAll(tasks);\n Console.WriteLine("Todas as tarefas concluídas.");\n }\n}\nVantagens: simplicidade, alta performance com grande número de operações.
\nDesvantagens: não possui mecanismo embutido de espera (necessário loop com verificação), difícil limitar o tamanho da fila.
2. BlockingCollection — fila com bloqueio e limite
\nBlockingCollection<T> é um wrapper sobre uma coleção thread-safe que adiciona a capacidade de bloquear a thread consumidora enquanto a fila estiver vazia, e a thread produtora enquanto a fila estiver cheia. Por padrão, usa ConcurrentQueue, mas é possível usar qualquer coleção que implemente IProducerConsumerCollection<T>.
Exemplo: processamento de pedidos com buffer limitado
\nImagine um sistema de processamento de pedidos. O produtor gera pedidos, e três consumidores os processam. O buffer é limitado a 10 elementos para não consumir muita memória.
\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 // Producer\n var producer = Task.Run(() =>\n {\n for (int i = 1; i <= 20; i++)\n