Partilhar via


Quando usar uma coleção thread-safe

O .NET Framework 4 introduziu cinco tipos de coleção que são especialmente projetados para oferecer suporte a operações de adição e remoção multi-threaded. Para alcançar a segurança da rosca, esses tipos usam vários tipos de bloqueio eficiente e mecanismos de sincronização sem bloqueio. A sincronização adiciona sobrecarga a uma operação. A quantidade de sobrecarga depende do tipo de sincronização usada, do tipo de operações executadas e de outros fatores, como o número de threads que estão tentando acessar simultaneamente a coleção.

Em alguns cenários, a sobrecarga de sincronização é insignificante e permite que o tipo multi-threaded tenha um desempenho significativamente mais rápido e dimensione muito melhor do que seu equivalente não seguro para threads quando protegido por um bloqueio externo. Em outros cenários, a sobrecarga pode fazer com que o tipo thread-safe execute e dimensione aproximadamente o mesmo ou até mais lentamente do que a versão do tipo bloqueada externamente e não segura para threads.

As seções a seguir fornecem orientação geral sobre quando usar uma coleção thread-safe versus seu 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 muitos fatores, a orientação não é específica e não é necessariamente válida em todas as circunstâncias. Se o desempenho for muito importante, a melhor maneira de determinar qual tipo de coleta usar é medir o desempenho com base em configurações e cargas representativas do computador. Este documento utiliza os seguintes termos:

Cenário puro produtor-consumidor
Qualquer thread determinado está adicionando ou removendo elementos, mas não ambos.

Cenário misto produtor-consumidor
Qualquer thread está adicionando e 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 escala executa mais rápido em oito núcleos do que em dois núcleos.

ConcurrentQueue(T) vs. Queue(T)

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

Quando o tempo de processamento é de cerca de 500 FLOPS (operações de ponto flutuante) ou mais, a regra de dois threads não se aplica ao ConcurrentQueue<T>, que então tem uma escalabilidade muito boa. Queue<T> não se dimensiona bem neste cenário.

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

ConcurrentStack vs. Pilha

Em cenários puros de produtor-consumidor, quando o tempo de processamento é muito pequeno, então System.Collections.Concurrent.ConcurrentStack<T> e System.Collections.Generic.Stack<T> que tem um bloqueio externo provavelmente terá o mesmo desempenho com um thread de empurrar dedicado e um thread de popping dedicado. No entanto, à medida que o número de threads aumenta, ambos os tipos diminuem devido ao aumento da contenção e Stack<T> podem ter um desempenho melhor do que ConcurrentStack<T>o . Quando o tempo de processamento é de cerca de 500 FLOPS ou mais, ambos os tipos são dimensionados aproximadamente na mesma taxa.

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

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

ConcurrentDictionary vs. Dicionário

Em geral, use um System.Collections.Concurrent.ConcurrentDictionary<TKey,TValue> em qualquer cenário em que você esteja adicionando e atualizando chaves ou valores simultaneamente de vários threads. Em cenários que envolvem atualizações frequentes e relativamente poucas leituras, o geralmente oferece benefícios modestos ConcurrentDictionary<TKey,TValue> . 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 grau de simultaneidade no e, em seguida, medir para ver se o ConcurrentDictionary<TKey,TValue> desempenho aumenta em computadores que têm mais núcleos. Se você alterar o nível de simultaneidade, evite operações globais tanto quanto possível.

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

Saco Simultâneo

Em cenários puramente produtor-consumidor, System.Collections.Concurrent.ConcurrentBag<T> provavelmente terá um desempenho mais lento do que os outros tipos de coleta simultânea.

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

BlockingCollection

Quando a semântica de limitação e bloqueio são necessárias, System.Collections.Concurrent.BlockingCollection<T> provavelmente terá um desempenho mais rápido do que qualquer implementação personalizada. Ele também suporta cancelamento avançado, enumeração e tratamento de exceções.

Consulte também