Pergunta

Eu era capaz de implementar um dicionário thread-safe em C # derivando IDictionary e definir um objeto SyncRoot privada:

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...
}

Eu, então, bloquear este objeto SyncRoot durante toda a minha consumidores (múltiplas threads):

Exemplo:

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

Eu era capaz de fazê-lo funcionar, mas isso resultou em algum código feio. A minha pergunta é, existe uma maneira melhor, mais elegante de implementar um dicionário thread-safe?

Foi útil?

Solução

Como Pedro disse, você pode encapsular toda a segurança de uma discussão dentro da classe. Você terá que ter cuidado com alguns eventos que você exponha ou adicionar, certificando-se que eles se invocado fora de quaisquer bloqueios.

public class SafeDictionary<TKey, TValue>: IDictionary<TKey, TValue>
{
    private readonly object syncRoot = new object();
    private Dictionary<TKey, TValue> d = new Dictionary<TKey, TValue>();

    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...
}

Editar: Os docs MSDN salientar que enumerando é inerentemente não thread-safe. Isso pode ser uma das razões para expor um objeto de sincronização fora da sua classe. Outra forma de abordagem que seria fornecer alguns métodos para a realização de uma ação em todos os membros e bloqueio em torno da enumeração dos membros. O problema com isso é que você não sabe se a ação passados ??para essa função chama algum membro de seu dicionário (que resultaria em um impasse). Expondo o objeto de sincronização permite que o consumidor para tomar essas decisões e não esconde o impasse dentro da sua classe.

Outras dicas

A classe .NET 4.0 que suporta a simultaneidade é nomeado ConcurrentDictionary .

A tentativa de sincronizar internamente quase certamente será insuficiente porque está em um nível muito baixo de abstração. Digamos que você tornar as operações Add e ContainsKey individualmente thread-safe como segue:

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 este bit supostamente thread-safe de código de vários segmentos? Será sempre trabalho OK?

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

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

Que meios para escrever este tipo de cenário será necessário um bloqueio fora dicionário, por exemplo.

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

Mas agora, vendo como você está tendo que escrever um código de bloqueio externamente, você está misturando a sincronização interna e externa, que sempre leva a problemas como o código claro e impasses. Então, em última análise, você é provavelmente melhor para qualquer um:

  1. Uso de um Dictionary<TKey, TValue> normal e sincronizar externamente, encerrando as operações de compostos em que, ou

  2. Escreva uma nova embalagem thread-safe com uma interface diferente (ou seja, não IDictionary<T>) que combina as operações como um método AddIfNotContained para que você nunca precisa combinar as operações a partir dele.

(I tendem a ir com # 1 eu)

Você não deve publicar o seu objeto de bloqueio privado através de uma propriedade. O objeto de bloqueio deve existir em privado com o único propósito de agir como um ponto de encontro.

Se o desempenho revela-se pobre utilizando o bloqueio de padrão, em seguida, coleção de fechaduras Poder Enfiar de Wintellect pode ser muito útil.

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

  1. Você não deve nunca expor o seu objeto de sincronização. Se o fizer, irá abrir-se a um consumidor pegar o objeto e tendo um bloqueio sobre ele e, em seguida, você está frito.
  2. Você está implementando uma interface segura non-thread com uma classe thread-safe. IMHO isso vai custar-lhe no caminho

Pessoalmente, eu encontrei a melhor maneira de implementar um fio classe segura é através de imutabilidade. É realmente reduz o número de problemas que você pode executar em com segurança de segmentos. Confira Blog do Eric Lippert para mais detalhes.

Você não precisa bloquear a propriedade SyncRoot em seus objetos de consumo. O bloqueio você tem dentro dos métodos do dicionário é suficiente.

Para elaborar: O que acaba acontecendo é que seu dicionário está bloqueado por um longo período de tempo do que o necessário.

O que acontece no seu caso é o seguinte:

Say thread A adquire o bloqueio na SyncRoot antes a chamada para m_mySharedDictionary.Add. Segmento B, em seguida, tenta adquirir o bloqueio, mas está bloqueado. Na verdade, todos os outros segmentos estão bloqueadas. Thread A tem permissão para chamar o método Add. Na declaração de bloqueio dentro do método Add, linha A tem permissão para obter o bloqueio novamente, porque ele já possui. Ao sair do contexto de bloqueio no interior do método e, em seguida, fora do método, Um fio lançou todos os bloqueios que permitem outros segmentos para continuar.

Você pode simplesmente permitir que qualquer consumidor para chamar o método Add como a declaração de bloqueio dentro de sua classe SharedDictionary método Add terá o mesmo efeito. Neste ponto no tempo, você tem bloqueio redundante. Você só iria bloquear SyncRoot fora de um dos métodos de dicionário se você tivesse que realizar duas operações no objeto dicionário que precisava ser garantida a ocorrer consecutivamente.

Apenas um pensamento porque não recriar o dicionário? Se a leitura é uma infinidade de escrever, então bloqueio irá sincronizar todas as solicitações.

exemplo

    private static readonly object Lock = new object();
    private static Dictionary<string, string> _dict = new Dictionary<string, string>();

    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<string, string>(_dict) { { key, returnValue } };

            return returnValue;
        }
    }

    public string GetValue(key)
    {
        string returnValue;

        return _dict.TryGetValue(key, out returnValue)? returnValue : Fetch(key);
    }
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top