Consegui implementar um dicionário thread-safe em C # derivando de IDictionary e definindo um objeto SyncRoot privado:
public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
private readonly object syncRoot = new object();
private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();
public object SyncRoot
{
get { return syncRoot; }
}
public void Add(TKey key, TValue value)
{
lock (syncRoot)
{
d.Add(key, value);
}
}
// more IDictionary members...
}
Em seguida, bloqueio este objeto SyncRoot em todos os meus consumidores (vários threads):
Exemplo:
lock (m_MySharedDictionary.SyncRoot)
{
m_MySharedDictionary.Add(...);
}
Consegui fazer funcionar, mas isso resultou em um código feio. Minha pergunta é: existe uma maneira melhor e mais elegante de implementar um Dicionário thread-safe?
Respostas:
Como Peter disse, você pode encapsular toda a segurança do thread dentro da classe. Você precisará ter cuidado com todos os eventos que expor ou adicionar, certificando-se de que sejam chamados fora de quaisquer bloqueios.
Edit: Os documentos do MSDN apontam que enumerar é inerentemente não thread-safe. Esse pode ser um motivo para expor um objeto de sincronização fora de sua classe. Outra maneira de abordar isso seria fornecer alguns métodos 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 aquela função chama algum membro do seu dicionário (isso resultaria em um deadlock). Expor o objeto de sincronização permite que o consumidor tome essas decisões e não esconde o impasse dentro de sua classe.
fonte
A classe .NET 4.0 que oferece suporte à simultaneidade é chamada
ConcurrentDictionary
.fonte
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
eContainsKey
individualmente thread-safe da seguinte maneira:Então, o que acontece quando você chama esse pedaço de código supostamente thread-safe a partir de vários threads? Sempre funcionará bem?
A resposta simples é não. Em algum ponto, o
Add
método lançará uma exceção indicando que a chave já existe no dicionário. Como pode ser isso com um dicionário thread-safe, você pode perguntar? Bem, só porque cada operação é segura para thread, a combinação de duas operações não é, já que outra thread poderia modificá-la entre sua chamada paraContainsKey
eAdd
.O que significa que para escrever este tipo de cenário corretamente, você precisa de um cadeado fora do dicionário, por exemplo
Mas agora, visto que você está tendo que escrever código de bloqueio externo, você está misturando a sincronização interna e externa, o que sempre leva a problemas como código pouco claro e deadlocks. Então, no final das contas, você provavelmente é melhor:
Use um normal
Dictionary<TKey, TValue>
e sincronize externamente, envolvendo as operações compostas nele, ouEscreva um novo wrapper thread-safe com uma interface diferente (ou seja, não
IDictionary<T>
) que combine as operações, como umAddIfNotContained
método, para que você nunca precise combinar as operações a partir dele.(Eu mesmo costumo ir com o nº 1)
fonte
Você não deve publicar seu objeto de bloqueio privado por meio de uma propriedade. O objeto de bloqueio deve existir em particular com o único propósito de atuar como um ponto de encontro.
Se o desempenho for ruim usando o bloqueio padrão, a coleção de bloqueios Power Threading do Wintellect pode ser muito útil.
fonte
Existem vários problemas com o método de implementação que você está descrevendo.
Pessoalmente, descobri que a melhor maneira de implementar uma classe thread-safe é por meio da imutabilidade. Isso realmente reduz o número de problemas que você pode encontrar com a segurança de thread. Confira o Blog de Eric Lippert para mais detalhes.
fonte
Você não precisa bloquear a propriedade SyncRoot em seus objetos de consumidor. O bloqueio que você tem nos métodos do dicionário é suficiente.
Para Elaborar: O que acaba acontecendo é que seu dicionário fica travado por mais tempo do que o necessário.
O que acontece no seu caso é o seguinte:
Say thread A adquire o bloqueio no SyncRoot antes da chamada para m_mySharedDictionary.Add. O thread B então tenta adquirir o bloqueio, mas é bloqueado. Na verdade, todos os outros threads estão bloqueados. O thread A pode chamar o método Add. Na instrução de bloqueio dentro do método Add, o encadeamento A tem permissão para obter o bloqueio novamente porque já o possui. Ao sair do contexto de bloqueio dentro do método e, em seguida, fora do método, o encadeamento A liberou todos os bloqueios permitindo que outros encadeamentos continuem.
Você pode simplesmente permitir que qualquer consumidor chame o método Add, pois a instrução de bloqueio dentro do método Add da sua classe SharedDictionary terá o mesmo efeito. Neste ponto no tempo, você tem bloqueio redundante. Você só travaria SyncRoot fora de um dos métodos de dicionário se tivesse que realizar duas operações no objeto de dicionário que precisassem ser garantidas para ocorrer consecutivamente.
fonte
Apenas uma ideia, por que não recriar o dicionário? Se a leitura for uma infinidade de gravações, o bloqueio sincronizará todas as solicitações.
exemplo
fonte
Coleções e sincronização
fonte