Share via


Quando usar uma coleção thread-safe

O .NET Framework 4 apresentou cinco tipos de coleção especialmente projetados para dar suporte a operações multithread de adição e remoção. Para obter o acesso thread-safe, esses tipos usam vários tipos de mecanismos de sincronização de bloqueio e sem bloqueio. A sincronização adiciona sobrecarga a uma operação. A quantidade de sobrecarga depende do tipo de sincronização usado, os tipos de operações que são executadas e outros fatores, como o número de threads que estão tentando acessar a coleção simultaneamente.

Em alguns cenários, a sobrecarga de sincronização é insignificante e permite que o tipo com multithread funcione significativamente mais rápido e ajuste a escala muito melhor do que seu equivalente não thread-safe quando protegido por um bloqueio externo. Em outros cenários, a sobrecarga pode fazer com o tipo thread-safe funcione e ajuste a escala na mesma velocidade ou até mais lentamente do que a versão bloqueada externamente não thread-safe do tipo.

As seções a seguir fornecem diretrizes gerais sobre quando usar uma coleção thread-safe versus sua equivalente não thread-safe que tem um bloqueio fornecido pelo usuário em torno de suas operações de leitura e gravação. Como o desempenho pode variar dependendo de vários fatores, as diretrizes não são específicas e não são necessariamente válidas em todas as circunstâncias. Se o desempenho for muito importante, a melhor maneira de determinar que tipo de coleção usar é medir o desempenho com base em cargas e configurações de computador representativas. Este documento usa os seguintes termos:

Cenário de produtor-consumidor puro
Um determinado thread está adicionando ou removendo elementos, mas não executando as duas ações.

Cenário de produtor-consumidor misto
Um determinado thread está tanto adicionando quanto removendo elementos.

Aceleração
Desempenho algorítmico mais rápido em relação a outro tipo no mesmo cenário.

Escalabilidade
O aumento no desempenho que é proporcional ao número de núcleos no computador. Um algoritmo que ajusta a escala tem um desempenho mais rápido em oito núcleos do que em dois núcleos.

ConcurrentQueue(T) vs. Queue(T)

Em cenários de produtor-consumidor puros, em que o tempo de processamento para cada elemento é muito pequeno (poucas instruções), System.Collections.Concurrent.ConcurrentQueue<T> pode oferecer benefícios de desempenho modestos em relação a um System.Collections.Generic.Queue<T> que tem um bloqueio externo. Nesse cenário, ConcurrentQueue<T> funciona melhor quando um thread dedicado está enfileirando e um está retirando da fila. Se você não impuser essa regra, Queue<T> poderá até mesmo funcionar um pouco mais rápido do que ConcurrentQueue<T> em computadores com vários núcleos.

Quando o tempo de processamento é em torno de 500 FLOPS (operações em ponto flutuante) ou mais, a regra de dois threads não se aplica a ConcurrentQueue<T>, que tem a escalabilidade muito boa. Queue<T> não ajusta a escala bem nesse cenário.

Em cenários de produtor-consumidor mistos, quando o tempo de processamento é muito pequeno, um Queue<T> que tem um bloqueio externo ajusta a escala melhor do que ConcurrentQueue<T>. No entanto, quando o tempo de processamento é em torno de 500 FLOPS ou mais, o ConcurrentQueue<T> ajusta a escala melhor.

ConcurrentStack vs. Stack

Em cenários de produtor-consumidor puros, quando o tempo de processamento é muito pequeno, System.Collections.Concurrent.ConcurrentStack<T> e System.Collections.Generic.Stack<T> que tenham um bloqueio externo provavelmente funcionarão praticamente da mesma forma com um thread de colocação dedicado e um thread de retirada dedicado. No entanto, conforme o número de threads aumenta, os dois tipos ficam mais lentos devido ao aumento da contenção e Stack<T> pode funcionar melhor do que ConcurrentStack<T>. Quando o tempo de processamento é em torno de 500 FLOPS ou mais, os dois tipos ajustam a escala aproximadamente na mesma taxa.

Em cenários de produtor-consumidor mistos, ConcurrentStack<T> é mais rápido para cargas de trabalho grandes e pequenas.

O uso de PushRange e TryPopRange pode acelerar muito os tempos de acesso.

ConcurrentDictionary vs. Dictionary

Em geral, use uma System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> em qualquer cenário em que estiver adicionando ou atualizando chaves ou valores simultaneamente de vários threads. Em cenários que envolvem atualizações frequentes e relativamente poucas leituras, o ConcurrentDictionary<TKey,TValue> geralmente oferece benefícios modestos. Em cenários que envolvem muitas leituras e muitas atualizações, o ConcurrentDictionary<TKey,TValue> geralmente é significativamente mais rápido em computadores que têm qualquer número de núcleos.

Em cenários que envolvem atualizações frequentes, você pode aumentar o nível de simultaneidade no ConcurrentDictionary<TKey,TValue> e, depois, medir para ver se o desempenho aumenta em computadores que têm mais núcleos. Se você alterar o nível de simultaneidade, evite operações globais o máximo possível.

Se você só estiver lendo chave ou valores, o Dictionary<TKey,TValue> será mais rápido porque nenhuma sincronização será necessária se o dicionário não estiver sendo modificado por nenhum thread.

ConcurrentBag

Em cenários de produtor-consumidor puros, System.Collections.Concurrent.ConcurrentBag<T> provavelmente funcionará mais lentamente do que os outros tipos de coleção simultâneas.

Em cenários produtor-consumidor mistos, ConcurrentBag<T> geralmente é muito mais rápido e mais escalonável do que qualquer outro tipo de coleção simultânea para cargas de trabalho grandes e pequenas.

BlockingCollection

Quando forem necessárias semânticas de delimitação e bloqueio, System.Collections.Concurrent.BlockingCollection<T> provavelmente funcionará mais rápido do que qualquer implementação personalizada. Ele também dá suporte ao cancelamento, enumeração e tratamento de exceções avançados.

Confira também