Pergunta

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

Atualmente, utilizo um Dictionary para conter 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 tem acesso sincronizado.

Recentemente descobri que havia um conjunto de coleções thread-safe no .NET 4.0, e isso parece ser muito agradável.Fiquei me perguntando qual seria a opção 'mais eficiente e mais fácil de gerenciar', já que tenho a opção entre ter um sistema normal Dictionary com acesso sincronizado, ou ter um ConcurrentDictionary que já é thread-safe.

Referência ao .NET 4.0 ConcurrentDictionary

Foi útil?

Solução

Uma coleção thread-safe vs.uma coleção não-threadsafe pode ser vista de uma maneira diferente.

Considere uma loja sem atendente, exceto no caixa.Você terá muitos problemas se as pessoas não agirem com responsabilidade.Por exemplo, digamos que um cliente tire uma lata de uma lata-pirâmide enquanto um balconista está construindo a pirâmide, o inferno iria explodir.Ou, e se dois clientes buscarem o mesmo item ao mesmo tempo, quem ganha?Haverá uma luta?Esta é uma coleção não threadsafe.Existem muitas maneiras de evitar problemas, mas todas elas exigem algum tipo de bloqueio, ou melhor, acesso explícito de uma forma ou de outra.

Por outro lado, considere uma loja com um balconista no balcão e você só poderá comprar através dele.Você entra na fila e pede um item, ele traz de volta para você e você sai da fila.Se precisar de vários itens, você só pode pegar tantos itens em cada viagem de ida e volta quanto puder lembrar, mas precisa ter cuidado para evitar monopolizar o funcionário, 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é o início da fila e perguntar ao funcionário "Você tem papel higiênico", e ele disser "Sim", e então você disser "Ok, eu' Entrarei em contato com você quando souber o quanto preciso", então, quando você voltar à frente da fila, a loja poderá estar esgotada.Este cenário não é evitado por uma coleção threadsafe.

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

Uma coleção não threadsafe não oferece tais garantias.Por exemplo, se você adicionar algo a uma árvore binária em um thread, enquanto outro thread 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, ela pode estar corrompida além da esperança.

Uma coleção threadsafe, entretanto, não garante que todas as operações sequenciais 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 no meio tree.Count e tree.First(), outro thread eliminou os nós restantes na árvore, o que significa First() retornará null.

Para este cenário, você precisa verificar se a coleção em questão tem uma maneira segura de obter o que deseja, talvez seja necessário reescrever o código acima ou bloquear.

Outras dicas

Você ainda precisa ter muito cuidado ao usar coleções thread-safe porque thread-safe não significa que você pode ignorar todos os problemas de threading.Quando uma coleção se anuncia como thread-safe, geralmente significa que ela permanece em um estado consistente mesmo quando vários threads estão lendo e gravando simultaneamente.Mas isso não significa que um único thread 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 thread poderia ter removido a chave).Você ainda precisa usar o bloqueio neste caso (ou melhor:combine as duas chamadas usando TryGetValue).

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

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

Eu diria, basta usar ConcurrentDictionary.

Acho que o método ConcurrentDictionary.GetOrAdd é exatamente o que a maioria dos cenários multithreaded precisa.

Você viu o Extensões reativas para .Net 3.5sp1.De acordo com Jon Skeet, eles portaram um pacote de extensões paralelas e estruturas de dados simultâneas para .Net3.5 sp1.

Existe um conjunto de exemplos para .Net 4 Beta 2, que descreve detalhadamente como usar as extensões paralelas.

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

Editar:.NET 4 ConcurrentDictionary e padrões.

A Microsoft lançou um pdf chamado Patterns of Paralell Programming.Realmente vale a pena fazer o download, pois descreve em detalhes muito interessantes os padrões corretos a serem usados ​​para extensões simultâneas .Net 4 e os anti-padrões a serem evitados. Aqui está.

Basicamente, você deseja usar o novo ConcurrentDictionary.Imediatamente, você precisa escrever menos código para criar programas thread-safe.

Usamos o ConcurrentDictionary para coleta em cache, que é preenchido novamente a cada 1 hora e lido por vários threads de cliente, semelhante para a solução para o Este exemplo de thread é seguro? pergunta.

Descobrimos que alterá-lo paraDicionário somente leituramelhor desempenho geral.

O ConcurrentDictionary é uma ótima opção se cumprir todos suas necessidades de segurança de thread.Se não for, em outras palavras, você está fazendo algo um pouco complexo, normal Dictionary+lock pode ser uma opção melhor.Por exemplo, digamos que você está adicionando alguns pedidos em um dicionário e deseja 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 é thread-safe.Vários threads atualizando o _totalAmount campo pode deixá-lo em um estado corrompido.Então 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 _totalAmount é consistente com as entradas do dicionário.Outro thread pode tentar ler esses valores para atualizar um elemento da UI:

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

O totalAmount pode não corresponder à contagem de pedidos no dicionário.As estatísticas exibidas podem estar erradas.Neste ponto você perceberá que deve estender o lock proteçã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);

Este código é perfeitamente seguro, mas todos os benefícios de usar um ConcurrentDictionary se foram.

  1. O desempenho tornou-se pior do que usar um normal Dictionary, pois o travamento interno dentro do ConcurrentDictionary agora é um desperdício e redundante.
  2. Você deve revisar todo o seu código para usos desprotegidos das variáveis ​​compartilhadas.
  3. Você está preso ao uso de uma API estranha (TryAdd?, AddOrUpdate?).

Então meu conselho é:comece com um Dictionary+lock, e mantenha a opção de atualizar posteriormente para um ConcurrentDictionary como uma otimização de desempenho, se esta opção for realmente viável.Em muitos casos não será.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top