Concorrência .NET: Escolhendo a Ferramenta Certa
Quando Usar Esta Skill
Use esta skill quando:
- Decidir como lidar com operações concorrentes no .NET
- Avaliar se deve usar async/await, Channels, Akka.NET ou outras abstrações
- Estiver tentado a usar locks, semáforos ou outros primitivos de sincronização
- Precisar processar fluxos de dados com backpressure, batching ou debouncing
- Gerenciar estado entre múltiplas entidades concorrentes
Arquivos de Referência
- concorrencia-avancada.md: Akka.NET Streams, Reactive Extensions, Akka.NET Actors (entity-per-actor, state machines, cluster sharding) e padrões de função local assíncrona
A Filosofia
Comece simples, escale apenas quando necessário.
A maioria dos problemas de concorrência pode ser resolvida com async/await. Recorra a ferramentas mais sofisticadas apenas quando tiver uma necessidade específica que o async/await não consiga atender de forma limpa.
Tente evitar estado mutável compartilhado. A melhor maneira de lidar com concorrência é projetá-la para longe. Dados imutáveis, passagem de mensagens e estado isolado (como atores) eliminam categorias inteiras de bugs.
Locks devem ser a exceção, não a regra. Quando não puder evitar estado mutável compartilhado:
- Primeira escolha: Redesenhe para evitá-lo (imutabilidade, passagem de mensagens, isolamento de ator)
- Segunda escolha: Use
System.Collections.Concurrent(ConcurrentDictionary, etc.) - Terceira escolha: Use
Channel<T>para serializar o acesso por meio de passagem de mensagens - Último recurso: Use
lockpara seções críticas simples e de curta duração
Árvore de Decisão
O que você está tentando fazer?
│
├─► Aguardar E/S (HTTP, banco de dados, arquivo)?
│ └─► Use async/await
│
├─► Processar uma coleção em paralelo (vinculado à CPU)?
│ └─► Use Parallel.ForEachAsync
│
├─► Padrão produtor/consumidor (fila de trabalho)?
│ └─► Use System.Threading.Channels
│
├─► Manipulação de eventos de UI (debounce, throttle, combinar)?
│ └─► Use Reactive Extensions (Rx)
│
├─► Processamento de fluxo no lado do servidor (backpressure, batching)?
│ └─► Use Akka.NET Streams
│
├─► Máquinas de estado com transições complexas?
│ └─► Use Akka.NET Actors (padrão Become)
│
├─► Gerenciar estado de muitas entidades independentes?
│ └─► Use Akka.NET Actors (entity-per-actor)
│
├─► Coordenar múltiplas operações assíncronas?
│ └─► Use Task.WhenAll / Task.WhenAny
│
└─► Nenhum dos acima se encaixa?
└─► Pergunte a si mesmo: "Eu realmente preciso de estado mutável compartilhado?"
├─► Sim → Considere redesenhar para evitá-lo
└─► Realmente inevitável → Use Channels ou Actors para serializar o acesso
Nível 1: async/await (Escolha Padrão)
Use para: Operações vinculadas a E/S, esperas não bloqueantes, maioria da concorrência do dia a dia.
// E/S assíncrona simples
public async Task<Order> GetOrderAsync(string orderId, CancellationToken ct)
{
var order = await _database.GetAsync(orderId, ct);
var customer = await _customerService.GetAsync(order.CustomerId, ct);
return order with { Customer = customer };
}
// Operações assíncronas paralelas (quando independentes)
public async Task<Dashboard> LoadDashboardAsync(string userId, CancellationToken ct)
{
var ordersTask = _orderService.GetRecentOrdersAsync(userId, ct);
var notificationsTask = _notificationService.GetUnreadAsync(userId, ct);
var statsTask = _statsService.GetUserStatsAsync(userId, ct);
await Task.WhenAll(ordersTask, notificationsTask, statsTask);
return new Dashboard(
Orders: await ordersTask,
Notifications: await notificationsTask,
Stats: await statsTask);
}
Princípios-chave: Sempre aceite CancellationToken. Use ConfigureAwait(false) em código de biblioteca. Não bloqueie em código assíncrono.
Nível 2: Parallel.ForEachAsync (Paralelismo Vinculado à CPU)
Use para: Processar coleções em paralelo quando o trabalho é vinculado à CPU ou você precisa de concorrência controlada.
public async Task ProcessOrdersAsync(
IEnumerable<Order> orders,
CancellationToken ct)
{
await Parallel.ForEachAsync(
orders,
new ParallelOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount,
CancellationToken = ct
},
async (order, token) =>
{
await ProcessOrderAsync(order, token);
});
}
Quando NÃO usar: Operações puramente de E/S, quando a ordem importa, quando você precisa de backpressure.
Nível 3: System.Threading.Channels (Produtor/Consumidor)
Use para: Filas de trabalho, padrões produtor/consumidor, desacoplamento de produtores de consumidores.
public class OrderProcessor
{
private readonly Channel<Order> _channel;
public OrderProcessor()
{
_channel = Channel.CreateBounded<Order>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
});
}
// Produtor
public async Task EnqueueOrderAsync(Order order, CancellationToken ct)
{
await _channel.Writer.WriteAsync(order, ct);
}
// Consumidor (executar como tarefa em segundo plano)
public async Task ProcessOrdersAsync(CancellationToken ct)
{
await foreach (var order in _channel.Reader.ReadAllAsync(ct))
{
await ProcessOrderAsync(order, ct);
}
}
public void Complete() => _channel.Writer.Complete();
}
Channels são bons para: Desacoplamento de velocidade, buffer com backpressure, fan-out para trabalhadores, filas em segundo plano.
Channels NÃO são bons para: Operações complexas de fluxo (batching, windowing), processamento por entidade com estado, supervisão sofisticada.
Nível 4+: Akka.NET Streams, Reactive Extensions, Actors
Para cenários avançados que exigem processamento de fluxo, composição de eventos de UI ou gerenciamento de entidades com estado, veja concorrencia-avancada.md.
Akka.NET Streams se destacam em batching, throttling e backpressure no lado do servidor. Reactive Extensions são ideais para composição de eventos de UI. Akka.NET Actors lidam com padrões entity-per-actor, máquinas de estado com Become() e sistemas distribuídos via Cluster Sharding.
Anti-Padrões: O que Evitar
Locks para Lógica de Negócio
// RUIM: Usando locks para proteger estado compartilhado
private readonly object _lock = new();
private Dictionary<string, Order> _orders = new();
public void UpdateOrder(string id, Action<Order> update)
{
lock (_lock) { if (_orders.TryGetValue(id, out var order)) update(order); }
}
// BOM: Use um ator ou Channel para serializar o acesso
Gerenciamento Manual de Threads
// RUIM: Criando threads manualmente
var thread = new Thread(() => ProcessOrders());
thread.Start();
// BOM: Use Task.Run ou abstrações melhores
_ = Task.Run(() => ProcessOrdersAsync(cancellationToken));
Bloqueio em Código Assíncrono
// RUIM: Bloqueando async - risco de deadlock!
var result = GetDataAsync().Result;
// BOM: Async até o fim
var result = await GetDataAsync();
Estado Mutável Compartilhado Sem Proteção
// RUIM: Múltiplas tarefas alterando estado compartilhado
var results = new List<Result>();
await Parallel.ForEachAsync(items, async (item, ct) =>
{
var result = await ProcessAsync(item, ct);
results.Add(result); // Condição de corrida!
});
// BOM: Use ConcurrentBag
var results = new ConcurrentBag<Result>();
Referência Rápida: Qual Ferramenta Quando?
| Necessidade | Ferramenta | Exemplo |
|---|---|---|
| Aguardar E/S | async/await | Chamadas HTTP, consultas a banco de dados |
| Trabalho paralelo em CPU | Parallel.ForEachAsync | Processamento de imagens, cálculos |
| Fila de trabalho | Channel<T> | Processamento de trabalhos em segundo plano |
| Eventos de UI com debounce/throttle | Reactive Extensions | Pesquisa enquanto digita, salvamento automático |
| Batching/throttling no servidor | Akka.NET Streams | Agregação de eventos, limitação de taxa |
| Máquinas de estado | Akka.NET Actors | Fluxos de pagamento, ciclos de vida de pedidos |
| Gerenciamento de estado de entidades | Akka.NET Actors | Gerenciamento de pedidos, sessões de usuário |
| Disparar múltiplas ops assíncronas | Task.WhenAll | Carregamento de dados de dashboard |
| Corrida de múltiplas ops assíncronas | Task.WhenAny | Timeout com fallback |
| Trabalho periódico | PeriodicTimer | Verificações de saúde, polling |
O Caminho de Escalonamento
async/await (comece aqui)
│
├─► Precisa de paralelismo? → Parallel.ForEachAsync
│
├─► Precisa de produtor/consumidor? → Channel<T>
│
├─► Precisa de composição de eventos de UI? → Reactive Extensions
│
├─► Precisa de processamento de fluxo no servidor? → Akka.NET Streams
│
└─► Precisa de máquinas de estado ou gerenciamento de entidades? → Akka.NET Actors
Escalone apenas quando tiver uma necessidade concreta. Não recorra a atores ou streams "por precaução".