Bloqueio de dicionário vs. ConcurrentDictionary


131

Não consegui encontrar informações suficientes sobre os ConcurrentDictionarytipos, então pensei em perguntar sobre isso aqui.

Atualmente, eu uso a Dictionarypara manter todos os usuários que são acessados ​​constantemente por vários threads (de um pool de threads, portanto, não há quantidade exata de threads), e ele tem acesso sincronizado.

Recentemente, descobri que havia um conjunto de coleções seguras para threads no .NET 4.0 e parece muito agradável. Fiquei me perguntando, qual seria a opção 'mais eficiente e mais fácil de gerenciar', pois tenho a opção entre ter um Dictionaryacesso normal com sincronizado ou um ConcurrentDictionaryque já seja seguro para threads.

Referência aos .NET 4.0 ConcurrentDictionary


Respostas:


146

Uma coleção segura para threads vs. uma coleção não segura para threads pode ser vista de uma maneira diferente.

Considere uma loja sem funcionário, exceto no check-out. Você tem muitos problemas se as pessoas não agirem com responsabilidade. Por exemplo, digamos que um cliente pegue uma lata de uma pirâmide - enquanto um funcionário está construindo a pirâmide, todo o inferno se abriria. Ou, se dois clientes alcançarem o mesmo item ao mesmo tempo, quem ganha? Haverá uma briga? Esta é uma coleção não segura para threads. Existem várias maneiras de evitar problemas, mas todas elas exigem algum tipo de bloqueio ou acesso explícito de uma maneira ou de outra.

Por outro lado, considere uma loja com um funcionário em uma mesa e você só pode fazer compras através dele. Você entra na fila e pede um item para ele, ele o devolve e você sai da fila. Se você precisar de vários itens, poderá coletar o máximo de itens em cada viagem de ida e volta, mas lembre-se de tomar cuidado, para não incomodar o atendente, pois isso irritará os outros clientes na fila atrás de você.

Agora considere isso. Na loja com um funcionário, e se você chegar até a frente da fila e perguntar ao funcionário "Você tem algum papel higiênico", e ele disser "Sim", e você disser "Ok, eu ' Entrarei em contato com você quando souber o quanto preciso "e, quando voltar à frente da fila, a loja poderá estar esgotada. Este cenário não é impedido por uma coleção threadsafe.

Uma coleção threadsafe garante que suas estruturas de dados internas sejam válidas o tempo todo, mesmo se acessadas de vários threads.

Uma coleção não segura para threads não possui essas garantias. Por exemplo, se você adicionar algo a uma árvore binária em um thread, enquanto outro estiver ocupado reequilibrando a árvore, não há garantia de que o item será adicionado ou mesmo que a árvore ainda seja válida depois, pode estar corrompida além da esperança.

Uma coleção threadsafe, no entanto, não garante que as operações seqüenciais no thread funcionem no mesmo "instantâneo" de sua estrutura de dados interna, o que significa que, se você tiver um código como este:

if (tree.Count > 0)
    Debug.WriteLine(tree.First().ToString());

você pode obter uma NullReferenceException porque, entre tree.Counte tree.First(), outro encadeamento limpou os nós restantes na árvore, o que significa First()que retornará null.

Nesse cenário, é necessário verificar se a coleção em questão possui uma maneira segura de obter o que deseja, talvez seja necessário reescrever o código acima ou talvez seja necessário bloquear.


9
Toda vez que leio este artigo, apesar de tudo verdade, estamos de alguma forma seguindo o caminho errado.
scope_creep

19
O principal problema em um aplicativo habilitado para encadeamento são estruturas de dados mutáveis. Se você puder evitar isso, poupará muitos problemas.
Lasse V. Karlsen

89
Esta é uma boa explicação sobre a segurança dos threads, no entanto, não posso deixar de sentir que isso realmente não abordou a questão do OP. O que o OP pediu (e o que eu encontrei posteriormente nessa pergunta) foi a diferença entre usar um padrão Dictionarye lidar com o bloqueio por conta própria versus o ConcurrentDictionarytipo que está embutido no .NET 4+. Na verdade, estou um pouco confuso que isso tenha sido aceito.
precisa saber é o seguinte

4
A segurança de threads é mais do que apenas usar a coleção certa. Usar a coleção certa é um começo, não é a única coisa com a qual você precisa lidar. Eu acho que é isso que o OP queria saber. Não sei por que isso foi aceito, já faz um tempo desde que escrevi isso.
Lasse V. Karlsen

2
Acho que o que LasseV.Karlsen está tentando dizer é que ... é fácil conseguir a segurança do thread, mas você deve fornecer um nível de simultaneidade na maneira como lida com essa segurança. Pergunte a si mesmo: "Posso fazer mais operações ao mesmo tempo, mantendo o objeto protegido por thread?" Considere este exemplo: Dois clientes desejam retornar itens diferentes. Quando você, como gerente, faz a viagem para reabastecer sua loja, não pode apenas reabastecer os dois itens em uma viagem e ainda lidar com as solicitações dos dois clientes, ou você faz uma viagem sempre que um cliente devolve um item?
Alexandru

68

Você ainda precisa ter muito cuidado ao usar coleções seguras para encadeamento, porque não é possível ignorar todos os problemas de encadeamento. Quando uma coleção se anuncia como segura para threads, geralmente significa que permanece em um estado consistente, mesmo quando vários threads estão lendo e gravando simultaneamente. Mas isso não significa que um único encadeamento verá uma sequência "lógica" de resultados se chamar vários métodos.

Por exemplo, se você primeiro verificar se existe uma chave e depois obter o valor que corresponde à chave, essa chave poderá não existir mais, mesmo com uma versão ConcurrentDictionary (porque outro encadeamento poderia ter removido a chave). Você ainda precisa usar o bloqueio nesse caso (ou melhor: combine as duas chamadas usando o TryGetValue ).

Portanto, use-os, mas não pense que isso lhe dá um passe livre para ignorar todos os problemas de simultaneidade. Você ainda precisa ter cuidado.


4
Então, basicamente, você está dizendo que se você não usar o objeto corretamente, ele não funcionará?
ChaosPandion

3
Basicamente, você precisa usar o TryAdd em vez dos comuns Contém -> Adicionar.
ChaosPandion

3
@Bruno: Não. Se você não bloqueou, outro segmento pode ter removido a chave entre as duas chamadas.
Mark Byers

13
Não quero parecer ríspido aqui, mas TryGetValuesempre fez parte Dictionarye sempre foi a abordagem recomendada. Os importantes métodos "simultâneos" que ConcurrentDictionaryintroduz são AddOrUpdatee GetOrAdd. Então, boa resposta, mas poderia ter escolhido um exemplo melhor.
Aaronaught 27/12/09

4
Certo - ao contrário do banco de dados que possui transações, a maioria das estruturas de pesquisa em memória simultâneas perdem o conceito de isolamento , elas apenas garantem que a estrutura de dados interna esteja correta.
828 Chang

39

Internamente, o ConcurrentDictionary usa um bloqueio separado para cada bloco de hash. Desde que você use apenas Add / TryGetValue e métodos semelhantes que funcionem em entradas únicas, o dicionário funcionará como uma estrutura de dados quase sem bloqueios, com os respectivos benefícios de desempenho. OTOH, os métodos de enumeração (incluindo a propriedade Count) bloqueiam todos os buckets de uma só vez e, portanto, são piores que um Dicionário sincronizado, em termos de desempenho.

Eu diria, basta usar ConcurrentDictionary.


15

Eu acho que o método ConcurrentDictionary.GetOrAdd é exatamente o que a maioria dos cenários com vários threads precisa.


1
Eu usaria o TryGet também, caso contrário, você desperdiçará o esforço em inicializar o segundo parâmetro para GetOrAdd toda vez que a chave já existir.
precisa

7
GetOrAdd tem a sobrecarga GetOrAdd (TKey, Func <TKey, TValue>) ; portanto, o segundo parâmetro pode ser inicializado com preguiça apenas no caso de a chave não existir no dicionário. Além TryGetValue é usado para buscar dados, não modificando. Chamadas subsequentes para cada um desses métodos podem causar que outro thread tenha adicionado ou removido a chave entre as duas chamadas.
sgnsajgon

Essa deve ser a resposta marcada para a pergunta, mesmo que o post de Lasse seja excelente.
Spivonious

13

Você já viu as extensões reativas para .Net 3.5sp1. Segundo Jon Skeet, eles fizeram o backport de um pacote de extensões paralelas e estruturas de dados simultâneas para .Net3.5 sp1.

Há um conjunto de exemplos para o .Net 4 Beta 2, que descreve com bastante detalhes sobre como usá-los nas extensões paralelas.

Acabei de passar a última semana testando o ConcurrentDictionary usando 32 threads para executar E / S. Parece funcionar como anunciado, o que indicaria uma enorme quantidade de testes realizados.

Edit : .NET 4 ConcurrentDictionary e padrões.

A Microsoft lançou um pdf chamado Patterns of Paralell Programming. Realmente vale a pena fazer o download, como descrito em detalhes muito bons, os padrões certos a serem usados ​​nas extensões concorrentes .Net 4 e os anti padrões a serem evitados. Aqui está.


4

Basicamente, você quer ir com o novo ConcurrentDictionary. Logo de cara, você precisa escrever menos código para criar programas seguros para threads.


existe algum problema se eu prosseguir com a dicção simultânea?
kbvishnu

1

A ConcurrentDictionaryé uma ótima opção se cumprir todas as suas necessidades thread-segurança. Se não estiver, em outras palavras, você está fazendo algo levemente complexo, um Dictionary+ normal lockpode ser uma opção melhor. Por exemplo, digamos que você esteja adicionando alguns pedidos a um dicionário e deseje manter atualizado o valor total dos pedidos. Você pode escrever um código como este:

private ConcurrentDictionary<int, Order> _dict;
private Decimal _totalAmount = 0;

while (await enumerator.MoveNextAsync())
{
    Order order = enumerator.Current;
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

Este código não é seguro para threads. Vários segmentos que atualizam o _totalAmountcampo podem deixá-lo em um estado corrompido. Portanto, você pode tentar protegê-lo com um lock:

_dict.TryAdd(order.Id, order);
lock (_locker) _totalAmount += order.Amount;

Este código é "mais seguro", mas ainda não é seguro para threads. Não há garantia de que isso _totalAmountseja consistente com as entradas no dicionário. Outro segmento pode tentar ler esses valores, para atualizar um elemento da interface do usuário:

Decimal totalAmount;
lock (_locker) totalAmount = _totalAmount;
UpdateUI(_dict.Count, totalAmount);

A totalAmountpodem não corresponder à contagem de pedidos no dicionário. As estatísticas exibidas podem estar erradas. Neste ponto, você perceberá que deve estender a lockproteção para incluir a atualização do dicionário:

// Thread A
lock (_locker)
{
    _dict.TryAdd(order.Id, order);
    _totalAmount += order.Amount;
}

// Thread B
int ordersCount;
Decimal totalAmount;
lock (_locker)
{
    ordersCount = _dict.Count;
    totalAmount = _totalAmount;
}
UpdateUI(ordersCount, totalAmount);

Esse código é perfeitamente seguro, mas todos os benefícios do uso de a ConcurrentDictionaryse foram.

  1. O desempenho piorou do que usar um normal Dictionary, porque o bloqueio interno dentro do ConcurrentDictionaryagora é desperdício e redundante.
  2. Você deve revisar todo o seu código para usos não protegidos das variáveis ​​compartilhadas.
  3. Você está preso ao usar uma API estranha ( TryAdd?, AddOrUpdate?).

Portanto, meu conselho é: comece com um Dictionary+ locke mantenha a opção de atualizar posteriormente para a ConcurrentDictionarycomo uma otimização de desempenho, se essa opção for realmente viável. Em muitos casos, não será.


0

Usamos o ConcurrentDictionary para coleção em cache, que é preenchida novamente a cada 1 hora e, em seguida, lida por vários threads do cliente, semelhante à solução para a thread Este exemplo de exemplo é seguro? questão.

Descobrimos que alterá-lo para  ReadOnlyDictionary  melhorou o desempenho geral.


ReadOnlyDictionary é apenas o invólucro de outro IDictionary. O que você usa debaixo do capô?
Lu55 5/10

@ Lu55, estávamos usando a biblioteca MS .Net e escolhemos entre os dicionários disponíveis / aplicáveis. Eu não estou ciente sobre a implementação interna do ReadOnlyDictionary pela Microsoft
Michael Freidgeim 4/18
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.