C#
- Apresentando ConcurrentDictionary
![]() |
Neste artigo veremos como funciona o ConcurrentDictionary na linguagem C#. |
A linguagem C# oferece uma variedade de coleções simultâneas para facilitar essa tarefa, e uma das mais versáteis entre elas é a ConcurrentDictionary.
O que é ConcurrentDictionary ?
A classe ConcurrentDictionary pertence à biblioteca System.Collections.Concurrent projetada para ser usada em cenários multithread que fornece uma implementação segura para operações concorrentes em dicionários. Ela foi projetado para ser usado em cenários onde vários threads acessam e modificam um dicionário simultaneamente.
A classe ConcurrentDictionary usa técnicas de bloqueio mais granulares, conhecidas como "striping", para permitir que várias operações ocorram simultaneamente em seções diferentes do dicionário, melhorando o desempenho em ambientes concorrentes.
Principais Métodos e Propriedades
Esta classe fornece uma variedade de métodos e propriedades úteis. Alguns dos principais são:
- AddOrUpdate : Adiciona uma chave e um valor ao dicionário ou atualiza o valor associado a uma chave existente.
concurrentDictionary.AddOrUpdate("Chave", valor, (chave, valorExistente) => novoValor);
- GetOrAdd : Obtém o valor associado a uma chave existente no dicionário ou adiciona uma nova chave e valor, se a chave não existir.
var valor = concurrentDictionary.GetOrAdd("Chave", valorPadrao);
- TryGetValue : Tenta obter o valor associado a uma chave, retornando verdadeiro se a operação foi bem-sucedida.
if (concurrentDictionary.TryGetValue("Chave", out var valor))
{
// Faça algo com o valor
}
|
- Count : Propriedade que retorna o número de pares chave-valor no dicionário.
int numeroDeElementos = concurrentDictionary.Count;
- Keys e Value : Propriedades que retornam coleções de chaves e valores, respectivamente.
var chaves = concurrentDictionary.Keys;
var valores = concurrentDictionary.Values;
|
Assim, a classe ConcurrentDictionary é útil em situações em que várias threads precisam acessar ou modificar um dicionário simultaneamente, garantindo um acesso seguro e eficiente. Sendo particularmente útil em cenários de programação concorrente, como em aplicações multi-threaded ou em situações em que a concorrência é necessária para otimizar o desempenho.
Capacidade Inicial
Ao inicializar um ConcurrentDictionary, você pode
especificar sua capacidade inicial que refere-se ao número inicial de slots que
o dicionário pode conter.
Especificar uma capacidade inicial pode ser benéfico se você tiver uma estimativa do número de itens que o dicionário conterá. Isso pode ajudar a reduzir o número de redimensionamentos, o que pode ser caro em termos de desempenho. A capacidade inicial padrão é 31 (um número primo)
using
System.Collections.Concurrent; int capacidadeInicial = 11;int nívelConcorrencia = Environment.ProcessorCount * 2;var concurrentDictionary = newConcurrentDictionary<int, string>(nívelConcorrencia, capacidadeInicial); |
Exemplo Prático
Imagine que você está construindo um serviço que calcula o fatorial de números. O cálculo fatorial pode ser demorado para números grandes. Para otimizar isso, você pode armazenar em cache os resultados de fatoriais calculados anteriormente usando um ConcurrentDictionary.
Vamos criar um projeto Console no .NET 8 chamado Cshp_ConcurrentDictionary e definir o código abaixo na classe Program:
using
System.Collections.Concurrent; var service = new FactorialService();Console.WriteLine("Calculando 5! ... "); //calcula o resultado e põe no cache // usa o resultado Console.WriteLine("\nUsando o resultado de 5! ... "); Console.WriteLine(service.CalculaFatorial(5)); Console.WriteLine("\nCalculando 7! ... "); Console.WriteLine(service.CalculaFatorial(7)); Console.ReadLine(); public class FactorialService{ private ConcurrentDictionary<int, long> _cache = new ConcurrentDictionary<int, long>(); public long CalculaFatorial(int numero) { if (numero < 0) { throw new ArgumentException("Número não pode ser negativo."); } // Se o resultadoado já esta no cache retorna if (_cache.TryGetValue(numero, out long cachedResult)) { Console.WriteLine($"Cache encontrado para : {numero}!"); return cachedResult; } // Calcula o fatorial long resultado = 1; for (int i = 1; i <= numero; i++) { resultado *= i; } // põe o resultadoado no cache _cache[numero] = resultado; return resultado; } } |
Resultado:
Criamos uma classe
FactorialService que possui um
ConcurrentDictionary chamado
_cache para armazenar os resultados de fatoriais calculados
anteriormente.
O método CalculaFactorial primeiro verifica se o
resultado do número fornecido já está no cache. Se for, ele retorna o resultado
armazenado em cache.
Se o resultado não estiver no cache, ele calcula o fatorial, armazena o
resultado em cache e depois o retorna.
Essa abordagem garante que os resultados de cálculos caros sejam armazenados em
cache e reutilizados, melhorando o desempenho do serviço. Além disso, o uso de
ConcurrentDictionary garante que o mecanismo de
cache seja seguro para threads, permitindo que vários threads calculem e
armazenem resultados em cache simultaneamente sem problemas.
Nem tudo são flores
Acontece que nem tudo são flores ao usar este recurso e você deverá atentar para os seguintes aspectos que podem estar envolvidos dependendo do cenário de uso :
Sobrecarga de simultaneidade:
ConcurrentDictionary foi projetado para segurança de thread. Para conseguir
isso, ele emprega bloqueio refinado e outros mecanismos de simultaneidade. Esses
mecanismos podem introduzir alguma sobrecarga em comparação com o Dicionário não
thread-safe. Em cenários com alta contenção (muitos threads tentando acessar
o dicionário simultaneamente), essa sobrecarga pode ser perceptível.
Sobrecarga de memória: ConcurrentDictionary
pode usar mais memória do que um dicionário normal devido às suas estruturas
internas projetadas para lidar com a simultaneidade.
Cenários de leitura pesada: Em cenários onde
há predominantemente operações de leitura com gravações mínimas, o
ConcurrentDictionary é otimizado para ter um desempenho muito bom. Muitas vezes,
as leituras podem ocorrer totalmente sem bloqueios, tornando-as muito rápidas.
Redimensionamento de bucket: Como qualquer
coleção baseada em hash, o desempenho pode ser prejudicado se houver muitas
colisões. No entanto, ConcurrentDictionary lida com o redimensionamento de seus
intervalos para distribuir as chaves e reduzir colisões. Esse redimensionamento
é seguro para threads, mas pode introduzir alguma sobrecarga durante as
operações de gravação, quando ocorre.
Obs: Um
"bucket" refere-se a uma estrutura de dados que armazena elementos com chaves
hash iguais ou colidentes. O
redimensionamento de bucket ocorre quando a tabela de dispersão precisa ser
redimensionada para acomodar mais elementos ou para reduzir a carga (load) da
tabela.
Pegue o código do
projeto aqui:
Cshp_ConcurrentDictionary.zip
"Ninguém vos engane com palavras vãs; porque por estas coisas vem a ira de
Deus sobre os filhos da desobediência. Portanto, não sejais seus companheiros.
Porque noutro tempo éreis trevas, mas agora sois luz no Senhor; andai como
filhos da luz"
Efésios
5:6-8
Referências:
C# - Tasks x Threads. Qual a diferença
DateTime - Macoratti.net
Null o que é isso ? - Macoratti.net
Formatação de data e hora para uma cultura ...
C# - Calculando a diferença entre duas datas
NET - Padrão de Projeto - Null Object Pattern
C# - Fundamentos : Definindo DateTime como Null ...
C# - Os tipos Nullable (Tipos Anuláveis)