Qual é a melhor maneira de implementar um dictionary seguro para thread?

Consegui implementar um Dictionary seguro em thread em C # derivando de IDictionary e definindo um object SyncRoot privado:

public class SafeDictionary: IDictionary { private readonly object syncRoot = new object(); private Dictionary d = new Dictionary(); public object SyncRoot { get { return syncRoot; } } public void Add(TKey key, TValue value) { lock (syncRoot) { d.Add(key, value); } } // more IDictionary members... } 

Em seguida, eu bloqueio esse object SyncRoot em todos os meus consumidores (vários segmentos):

Exemplo:

 lock (m_MySharedDictionary.SyncRoot) { m_MySharedDictionary.Add(...); } 

Consegui fazê-lo funcionar, mas isso resultou em algum código feio. Minha pergunta é, existe uma maneira melhor e mais elegante de implementar um dictionary seguro para thread?

Como o Peter disse, você pode encapsular toda a segurança do thread dentro da class. Você precisará ter cuidado com os events que expor ou adicionar, certificando-se de que eles sejam invocados fora de qualquer bloqueio.

 public class SafeDictionary: IDictionary { private readonly object syncRoot = new object(); private Dictionary d = new Dictionary(); public void Add(TKey key, TValue value) { lock (syncRoot) { d.Add(key, value); } OnItemAdded(EventArgs.Empty); } public event EventHandler ItemAdded; protected virtual void OnItemAdded(EventArgs e) { EventHandler handler = ItemAdded; if (handler != null) handler(this, e); } // more IDictionary members... } 

Edit: Os documentos do MSDN apontam que enumerar é inerentemente não thread-safe. Isso pode ser um motivo para expor um object de synchronization fora de sua class. Outra maneira de abordar isso seria fornecer alguns methods para executar uma ação em todos os membros e bloquear a enumeração dos membros. O problema com isso é que você não sabe se a ação passada para essa function chama algum membro do seu dictionary (isso resultaria em um deadlock). A exposição do object de synchronization permite que o consumidor tome essas decisões e não esconda o impasse dentro de sua class.

A class .NET 4.0 que suporta a simultaneidade é denominada ConcurrentDictionary .

Tentar sincronizar internamente quase certamente será insuficiente, porque está em um nível de abstração muito baixo. Digamos que você torne as operações Add e ContainsKey individualmente seguras para thread da seguinte maneira:

 public void Add(TKey key, TValue value) { lock (this.syncRoot) { this.innerDictionary.Add(key, value); } } public bool ContainsKey(TKey key) { lock (this.syncRoot) { return this.innerDictionary.ContainsKey(key); } } 

Então o que acontece quando você chama esse código supostamente thread-safe de vários threads? Será que vai funcionar sempre bem?

 if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); } 

A resposta simples é não. Em algum momento, o método Add lançará uma exceção indicando que a chave já existe no dictionary. Como isso pode ser com um dictionary thread-safe, você pode perguntar? Bem, só porque cada operação é thread-safe, a combinação de duas operações não é, como outro thread poderia modificá-lo entre sua chamada para ContainsKey e Add .

O que significa escrever este tipo de cenário corretamente você precisa de um bloqueio fora do dictionary, por exemplo

 lock (mySafeDictionary) { if (!mySafeDictionary.ContainsKey(someKey)) { mySafeDictionary.Add(someKey, someValue); } } 

Mas agora, como você está tendo que escrever código de bloqueio externo, você está misturando a synchronization interna e externa, o que sempre leva a problemas como código não definido e conflitos. Então, finalmente, você provavelmente é melhor:

  1. Use um Dictionary normal Dictionary e sincronize externamente, incluindo as operações compostas nele ou

  2. Escrever um novo wrapper thread-safe com uma interface diferente (ou seja, não IDictionary ) que combina as operações, como um método AddIfNotContained para que você nunca precise combinar operações dele.

(Eu costumo ir com o # 1 eu mesmo)

Você não deve publicar seu object de bloqueio privado por meio de uma propriedade. O object de bloqueio deve existir em particular com o único propósito de atuar como um ponto de encontro.

Se o desempenho se mostrar ruim usando o bloqueio padrão, a coleção de bloqueios Power Threading da Wintellect pode ser muito útil.

Existem vários problemas com o método de implementação que você está descrevendo.

  1. Você nunca deve expor seu object de synchronization. Ao fazer isso, você se abrirá para um consumidor pegando o object, fechando-o e fazendo um brinde.
  2. Você está implementando uma interface segura sem thread com uma class segura de thread. IMHO isso vai te custar na estrada

Pessoalmente, descobri que a melhor maneira de implementar uma class segura de thread é através da imutabilidade. Isso realmente reduz o número de problemas que você pode encontrar com segurança de thread. Confira o blog de Eric Lippert para mais detalhes.

Você não precisa bloquear a propriedade SyncRoot nos seus objects de consumidor. O bloqueio que você tem dentro dos methods do dictionary é suficiente.

Elaborar: O que acaba acontecendo é que o seu dictionary está bloqueado por um período de tempo maior do que o necessário.

O que acontece no seu caso é o seguinte:

Digamos que o thread A adquira o bloqueio no SyncRoot antes da chamada para m_mySharedDictionary.Add. O encadeamento B tenta, então, adquirir o bloqueio, mas é bloqueado. Na verdade, todos os outros segmentos estão bloqueados. O segmento A tem permissão para chamar o método Add. Na instrução de bloqueio dentro do método Add, o thread A tem permissão para obter o bloqueio novamente porque ele já o possui. Ao sair do contexto de bloqueio dentro do método e, em seguida, fora do método, o thread A liberou todos os bloqueios, permitindo que outros threads continuem.

Você pode simplesmente permitir que qualquer consumidor chame o método Add, já que a instrução de bloqueio no método Add da class SharedDictionary terá o mesmo efeito. Neste momento, você tem um bloqueio redundante. Você só bloquearia o SyncRoot fora de um dos methods de dictionary se tivesse que executar duas operações no object de dictionary que precisavam ser garantidas para ocorrerem consecutivamente.

Apenas um pensamento porque não recriar o dictionary? Se a leitura for uma infinidade de escrita, o bloqueio sincronizará todas as solicitações.

exemplo

  private static readonly object Lock = new object(); private static Dictionary _dict = new Dictionary(); private string Fetch(string key) { lock (Lock) { string returnValue; if (_dict.TryGetValue(key, out returnValue)) return returnValue; returnValue = "find the new value"; _dict = new Dictionary(_dict) { { key, returnValue } }; return returnValue; } } public string GetValue(key) { string returnValue; return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key); } 

Coleções e Sincronização