1. Introdução
ConcurrentBag<T> é uma coleção thread-safe e não ordenada. A principal característica e vantagem está na palavra "Bag" (saco), que implica que a ordem dos elementos não é garantida ao removê-los. Isso significa que o elemento que você retirar pode não ser aquele que você esperava com base na ordem de adição. Em compensação, ConcurrentBag tem uma otimização única que a torna extremamente rápida em certos cenários.
Características do ConcurrentBag
Sem ordenação: Ao contrário de filas (FIFO) e pilhas (LIFO), ConcurrentBag não garante que TryTake() retornará um elemento em qualquer ordem específica relativa à adição. Esta é uma diferença chave.
Otimização para acesso local (Thread-Local Storage): A razão principal da existência do ConcurrentBag é seu desempenho em cenários onde a thread que adicionou o elemento tem alta probabilidade de ser a mesma thread que irá removê-lo.
Exemplo: ConcurrentBag — adicionar e remover
using System.Collections.Concurrent;
ConcurrentBag<string> itemBag = new ConcurrentBag<string>();
// Adicionando itens
itemBag.Add("Ponto A");
itemBag.Add("Ponto B");
itemBag.Add("Ponto C");
Console.WriteLine($"Itens no saco: {itemBag.Count}"); // Saída: Itens no saco: 3
// Removendo itens (a ordem não é garantida!)
if (itemBag.TryTake(out string item1))
{
Console.WriteLine($"Removido: {item1}"); // Pode ser "Ponto C", "Ponto B" ou "Ponto A"
}
if (itemBag.TryTake(out string item2))
{
Console.WriteLine($"Removido: {item2}");
}
Console.WriteLine($"Itens restantes: {itemBag.Count}"); // Saída: Itens restantes: 1
Você pode executar esse código várias vezes e notar que a ordem dos itens removidos pode variar.
Métodos Add(), TryTake()
Add(T item): usado para adicionar um item ao ConcurrentBag. A operação é thread-safe.
TryTake(out T item): tentativa de remover um item do ConcurrentBag. Retorna true se um item foi removido com sucesso, e false se o saco estiver vazio. Importante: TryTake não bloqueia a thread.
2. Cenários de uso
ConcurrentBag não substitui ConcurrentQueue ou ConcurrentStack. Ela brilha em casos específicos:
Pools de objetos/recursos: quando você tem um pool de objetos reutilizáveis, e é desejável que a thread que devolveu o objeto seja a mesma que o pegue novamente com mais frequência. Isso reduz a contenção por recursos compartilhados.
Distribuição dinâmica de tarefas no TPL: a implementação interna de construções como Parallel.ForEach e Parallel.For usa sacos locais e um mecanismo de "work-stealing" para distribuir trabalho de forma eficiente.
Pool de tarefas com ConcurrentBag e otimização de localidade
using System.Collections.Concurrent;
using System.Threading.Tasks;
using System.Threading;
ConcurrentBag<string> taskPool = new ConcurrentBag<string>();
// Preenchemos o pool com tarefas iniciais
for (int i = 0; i < 10; i++)
{
taskPool.Add($"Tarefa {i}");
}
void Worker()
{
// Cada thread tenta pegar uma tarefa
while (taskPool.TryTake(out string task))
{
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: Processando {task}");
Thread.Sleep(50); // Simulação de trabalho
}
Console.WriteLine($"Thread {Thread.CurrentThread.ManagedThreadId}: Finalizou.");
}
// Iniciar algumas threads/trabalhadores
// Task.Run(Worker);
// Task.Run(Worker);
// Task.Run(Worker);
// Thread.Sleep(1000); // Dá tempo para execução
Neste exemplo, ConcurrentBag permite que as threads peguem tarefas de forma eficiente, minimizando bloqueios graças à sua estrutura interna.
Mecânica interna
ConcurrentBag alcança alto desempenho usando armazenamento local por thread (TLS). Quando uma thread adiciona um item, ele vai para uma estrutura local daquela thread. Ao chamar TryTake(), primeiro é verificada a estrutura local; se estiver vazia — ocorre "work-stealing" de outras threads ou do pool global. Isso reduz a contenção e torna a coleção ótima quando a localidade de acesso é importante e a ordem não.
3. Dicionário thread-safe
ConcurrentDictionary<TKey, TValue> é uma das coleções thread-safe mais usadas: um dicionário de alto desempenho para operações seguras de adicionar, ler, atualizar e remover a partir de múltiplas threads.
Um Dictionary<TKey, TValue> comum não é thread-safe. Qualquer escrita (adição/alteração/remoção) ou até leitura durante uma escrita pode causar exceções (InvalidOperationException) ou corrupção de dados.
Exemplo: problema com um Dictionary comum (repetição)
using System.Collections.Generic;
using System.Threading.Tasks;
Dictionary<int, int> concurrentDictProblem = new Dictionary<int, int>();
void AddToDict(int start, int count)
{
for (int i = 0; i < count; i++)
{
// Tentativa de adicionar/alterar simultaneamente
// Vai causar exceções ou comportamento incorreto
concurrentDictProblem[start + i] = start + i;
}
}
// Execução do exemplo no Main:
try
{
Task t1 = Task.Run(() => AddToDict(0, 10000));
Task t2 = Task.Run(() => AddToDict(5000, 10000)); // Sobreposição de chaves
Task.WaitAll(t1, t2);
Console.WriteLine($"Itens no dicionário (problemático): {concurrentDictProblem.Count}");
}
catch (Exception ex)
{
Console.WriteLine($"Erro no dicionário comum: {ex.Message}");
}
Esse código quase certamente vai lançar uma exceção ou travar por problemas de thread-safety.
4. Operações principais
ConcurrentDictionary fornece operações atômicas especializadas de "verificar + agir".
TryAdd(TKey key, TValue value): adiciona atômicamente um par chave-valor. Retorna true se a chave foi adicionada, e false se a chave já existe.
ConcurrentDictionary<string, int> scores = new ConcurrentDictionary<string, int>();
if (scores.TryAdd("Alice", 100))
Console.WriteLine("Alice adicionada."); // Saída: Alice adicionada.
if (!scores.TryAdd("Alice", 150))
Console.WriteLine("Alice já existe."); // Saída: Alice já existe.
TryGetValue(TKey key, out TValue value): obtém atômicamente o valor por chave.
if (scores.TryGetValue("Alice", out int aliceScore))
Console.WriteLine($"Placar da Alice: {aliceScore}"); // Saída: Placar da Alice: 100
TryUpdate(TKey key, TValue newValue, TValue comparisonValue): atualiza atômicamente o valor somente se o atual for igual a comparisonValue. Evita races.
// Valor atual da Alice = 100
if (scores.TryUpdate("Alice", 120, 100)) // Vai atualizar 100 para 120
Console.WriteLine("Placar da Alice atualizado para 120."); // Saída: Placar da Alice atualizado para 120.
if (!scores.TryUpdate("Alice", 130, 100)) // Não vai atualizar, pois o atual é 120, não 100
Console.WriteLine("Placar da Alice não atualizado (dados desatualizados)."); // Saída: ...
TryRemove(TKey key, out TValue value): remove atômicamente o elemento pela chave.
if (scores.TryRemove("Alice", out int removedScore))
Console.WriteLine($"Alice removida, o placar era: {removedScore}"); // Saída: Alice removida, o placar era: 120
5. Operações atômicas avançadas
Esses dois métodos são os cavalos de batalha do ConcurrentDictionary, cobrindo muitos cenários.
GetOrAdd(TKey key, TValue valueFactory(TKey key)): retorna atômicamente o valor existente pela chave ou cria e adiciona um novo via factory. Ideal para caches e entidades únicas.
// Suponha que estamos cacheando objetos pesados
ConcurrentDictionary<int, HeavyObject> objectCache = new ConcurrentDictionary<int, HeavyObject>();
HeavyObject GetOrCreateHeavyObject(int id)
{
// Se já existe — retorna, caso contrário cria e adiciona
return objectCache.GetOrAdd(id, (key) =>
{
Console.WriteLine($"Criando novo HeavyObject para ID: {key}");
return new HeavyObject(key); // Simulação de criação custosa
});
}
// No Main:
HeavyObject obj1 = GetOrCreateHeavyObject(1); // Vai criar novo
HeavyObject obj2 = GetOrCreateHeavyObject(2); // Vai criar novo
HeavyObject obj3 = GetOrCreateHeavyObject(1); // Vai retornar o obj1 existente
AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory): adiciona atômicamente o valor se a chave estiver ausente, ou atualiza o existente via factory.
- addValue: valor a adicionar se a chave não for encontrada.
- updateValueFactory: função que calcula o novo valor com base na chave e no valor atual.
// Contagem de visitas por página
ConcurrentDictionary<string, int> pageViews = new ConcurrentDictionary<string, int>();
void IncrementPageView(string page)
{
pageViews.AddOrUpdate(page, 1, // Se a página é nova, adiciona 1
(key, existingVal) => existingVal + 1); // Caso contrário incrementa em 1
Console.WriteLine($"Página '{page}' visitada {pageViews[page]} vezes.");
}
// No Main:
IncrementPageView("Home"); // Home: 1
IncrementPageView("About"); // About: 1
IncrementPageView("Home"); // Home: 2
IncrementPageView("Home"); // Home: 3
IncrementPageView("Contact"); // Contact: 1
6. Exemplos de uso para cache ou gerenciamento de estado
Cache de dados: ConcurrentDictionary é uma ótima escolha para cache in-memory: GetOrAdd evita recriar objetos custosos.
Gerenciamento de sessões de usuário: armazenar e atualizar com segurança dados de sessão vindos de diferentes requisições.
Contagem de estatísticas: com AddOrUpdate é fácil incrementar contadores de eventos, visualizações, votos etc.
Registries/Service Locator: armazenamento de serviços ou plugins registrados e acessíveis a partir de várias threads.
ConcurrentDictionary<TKey, TValue> é uma coleção altamente otimizada que simplifica bastante o desenvolvimento multithread com dicionários, fornecendo um conjunto de operações atômicas sem precisar de sincronização manual.
GO TO FULL VERSION