No desenvolvimento de aplicações web de alta concorrência, delegar operações demoradas para execução em segundo plano é uma prática essencial para manter a responsividade da interface. Uma abordagem inicial comum envolve a instancaição direta de novas threads para cada requisição recebida:
// Abordagem ingênua: criação excessiva de threads
new Thread(() =>
{
// Processamento pesado
}).Start();
Em abmientes com alto volume de tráfego, essa estratégia é insustentável. A sobrecarga gerada pela criação e destruição contínua de threads rapidamente exaure os recursos do processador, podendo levar a aplicação à falha completa. O mecanismo padrão para mitigar este problema no .NET é o ThreadPool.QueueUserWorkItem, que reutiliza threads ociosas e gerencia uma fila interna de execução.
// Utilizando o pool de threads nativo
ThreadPool.QueueUserWorkItem(state =>
{
// Lógica de negócio
}, null);
Embora o ThreadPool seja significativamente mais eficiente, ele ainda enfrenta desafios sob cargas extremas. O pool pode expandir o número de threads ativas para atender à demanda de forma agressiva, o que continua gerando alto consumo de memória e alternância de contexto (context switching) no sistema operacional.
Uma alternativa arquitetural para cenários onde o controle estrito de recursos é prioritário consiste em implementar o padrão Produtor-Consumidor utilizando uma única thread de trabalho dedicada. Isso elimina completamente a sobrecarga de múltiplas threads, procsesando as tarefas de forma sequencial. Abaixo, apresentamos uma implementação moderna utilizando BlockingCollection<T> e CancellationToken, substituindo o uso ineficiente de Thread.Sleep e blocos lock manuais encontrados em implementações legadas.
using System;
using System.Collections.Concurrent;
using System.Threading;
public class SequentialTaskExecutor : IDisposable
{
private readonly BlockingCollection<workitem> _taskQueue = new BlockingCollection<workitem>();
private readonly Thread _workerThread;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
private class WorkItem
{
public Action<object> Action { get; }
public object State { get; }
public WorkItem(Action<object> action, object state)
{
Action = action;
State = state;
}
}
public SequentialTaskExecutor()
{
_workerThread = new Thread(ProcessQueue)
{
IsBackground = true,
Name = "SequentialWorker"
};
_workerThread.Start();
}
private void ProcessQueue()
{
try
{
// GetConsumingEnumerable bloqueia a thread nativamente sem consumir CPU quando a fila está vazia
foreach (var item in _taskQueue.GetConsumingEnumerable(_cts.Token))
{
try
{
item.Action(item.State);
}
catch (Exception ex)
{
Console.WriteLine($"[SequentialTaskExecutor] Erro na tarefa: {ex.Message}");
}
}
}
catch (OperationCanceledException)
{
// Encerramento gracioso da thread
}
}
public void Enqueue(Action<object> action, object state)
{
if (!_taskQueue.IsAddingCompleted)
{
_taskQueue.Add(new WorkItem(action, state));
}
}
public void Dispose()
{
_taskQueue.CompleteAdding();
_cts.Cancel();
_workerThread.Join();
_cts.Dispose();
_taskQueue.Dispose();
}
}</object></object></object></workitem></workitem>
Essa implementação garante que apenas uma thread esteja ativa consumindo a fila. A utilização é direta:
using (var executor = new SequentialTaskExecutor())
{
executor.Enqueue(state =>
{
Console.WriteLine($"Tarefa processada em: {state}");
}, DateTime.UtcNow);
}
Para cenários que exigem processadores dedicados e fortemente tipados por tipo de dado, a solução pode ser generalizada utilizando genéricos:
using System;
using System.Collections.Concurrent;
using System.Threading;
public class TypedTaskProcessor<t> : IDisposable
{
private readonly Action<t> _handler;
private readonly BlockingCollection<t> _queue = new BlockingCollection<t>();
private readonly Thread _worker;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public TypedTaskProcessor(Action<t> handler)
{
_handler = handler ?? throw new ArgumentNullException(nameof(handler));
_worker = new Thread(Consume)
{
IsBackground = true
};
_worker.Start();
}
private void Consume()
{
try
{
foreach (var payload in _queue.GetConsumingEnumerable(_cts.Token))
{
try
{
_handler(payload);
}
catch
{
// Tratamento de exceções do domínio
}
}
}
catch (OperationCanceledException) { }
}
public void Submit(T data)
{
if (!_queue.IsAddingCompleted)
{
_queue.Add(data);
}
}
public void Dispose()
{
_queue.CompleteAdding();
_cts.Cancel();
_worker.Join();
_queue.Dispose();
_cts.Dispose();
}
}</t></t></t></t></t>
Exemplo de submissão de carga para processamento em lote de entidades:
public class BlogPost
{
public int Id { get; set; }
}
var processor = new TypedTaskProcessor<blogpost>(post =>
{
Console.WriteLine($"Indexando post ID: {post.Id}");
});
for (int i = 1; i <= 1000; i++)
{
processor.Submit(new BlogPost { Id = i });
}</blogpost>
A arquitetura baseada em fila de thread única oferece controle absoluto sobre a alocação de recursos do sistema. No entanto, a execução estritamente sequencial introduz latência acumulativa para itens posicionados no final de filas extensas. Adicionalmente, o armazenamento da fila em memória volátil implica que interrupções abruptas do processo hospedeiro, como reinicializações do IIS, resultarão na perda imediata de qualquer carga de trabalho pendente de processamento.