HashSet simultâneo <T> no .NET Framework?

151

Eu tenho a seguinte turma.

class Test{
    public HashSet<string> Data = new HashSet<string>();
}

Preciso alterar o campo "Dados" de diferentes threads, portanto, gostaria de algumas opiniões sobre minha implementação atual segura de thread.

class Test{
    public HashSet<string> Data = new HashSet<string>();

    public void Add(string Val){
            lock(Data) Data.Add(Val);
    }

    public void Remove(string Val){
            lock(Data) Data.Remove(Val);
    }
}

Existe uma solução melhor para ir diretamente ao campo e protegê-lo do acesso simultâneo por vários threads?

kukab
fonte
Que tal usar uma das coleções emSystem.Collections.Concurrent
I4V
8
Claro, torne-o privado.
Hans Passant
3
De uma perspectiva de concorrência, não vejo muita coisa errada com o que você fez, além de o campo Dados ser público! Você pode obter melhor desempenho de leitura usando o ReaderWriterLockSlim, se isso for uma preocupação. msdn.microsoft.com/pt-br/library/…
Allan Elder,
O @AllanElder ReaderWriterLockserá útil (eficiente) quando vários leitores e um único escritor. Temos de saber se este é o caso de OP
Sriram Sakthivel
2
A implementação atual não é realmente 'simultânea' :) É apenas thread-safe.
undefined

Respostas:

164

Sua implementação está correta. Infelizmente, o .NET Framework não fornece um tipo de hashset simultâneo interno. No entanto, existem algumas soluções alternativas.

ConcurrentDictionary (recomendado)

Este primeiro é usar a classe ConcurrentDictionary<TKey, TValue>no espaço para nome System.Collections.Concurrent. No caso, o valor é inútil, portanto, podemos usar um simples byte(1 byte na memória).

private ConcurrentDictionary<string, byte> _data;

Essa é a opção recomendada porque o tipo é seguro para threads e oferece as mesmas vantagens que um HashSet<T> chave e um valor exceto objetos diferentes.

Fonte: Social MSDN

ConcurrentBag

Se você não se importa com as entradas duplicadas, pode usar a classe ConcurrentBag<T>no mesmo espaço para nome da classe anterior.

private ConcurrentBag<string> _data;

Auto-implementação

Por fim, como você fez, você pode implementar seu próprio tipo de dados, usando o lock ou outras formas que o .NET fornece para você ser seguro para threads. Aqui está um ótimo exemplo: Como implementar o ConcurrentHashSet no .Net

A única desvantagem dessa solução é que o tipo HashSet<T>não tem acesso simultâneo oficial, mesmo para operações de leitura.

Cito o código do post vinculado (originalmente escrito por Ben Mosher ).

using System;
using System.Collections.Generic;
using System.Threading;

namespace BlahBlah.Utilities
{
    public class ConcurrentHashSet<T> : IDisposable
    {
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
        private readonly HashSet<T> _hashSet = new HashSet<T>();

        #region Implementation of ICollection<T> ...ish
        public bool Add(T item)
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Add(item);
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public void Clear()
        {
            _lock.EnterWriteLock();
            try
            {
                _hashSet.Clear();
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public bool Contains(T item)
        {
            _lock.EnterReadLock();
            try
            {
                return _hashSet.Contains(item);
            }
            finally
            {
                if (_lock.IsReadLockHeld) _lock.ExitReadLock();
            }
        }

        public bool Remove(T item)
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Remove(item);
            }
            finally
            {
                if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }
        }

        public int Count
        {
            get
            {
                _lock.EnterReadLock();
                try
                {
                    return _hashSet.Count;
                }
                finally
                {
                    if (_lock.IsReadLockHeld) _lock.ExitReadLock();
                }
            }
        }
        #endregion

        #region Dispose
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
                if (_lock != null)
                    _lock.Dispose();
        }
        ~ConcurrentHashSet()
        {
            Dispose(false);
        }
        #endregion
    }
}

EDIT: Mova os métodos de bloqueio de entrada para fora dos tryblocos, pois eles podem lançar uma exceção e executar as instruções contidas nos finallyblocos.

ZenLulz
fonte
8
um dicionário com os valores da sucata é uma lista
Ralf
44
@ Ralf Bem, é um conjunto, não uma lista, pois não é ordenado.
Servy
11
De acordo com o documento bastante curto do MSDN sobre "Coleções e sincronização (segurança de threads)" , as classes no System.Collections e os namespaces relacionados podem ser lidos com segurança por vários threads. Isso significa que o HashSet pode ser lido com segurança por vários threads.
Hank Schultz
7
@ Oliver, uma referência usa muito mais memória por entrada, mesmo que seja uma nullreferência (a referência precisa de 4 bytes em tempo de execução de 32 bits e 8 bytes em tempo de execução de 64 bits). Portanto, o uso de a byte, uma estrutura vazia ou semelhante pode reduzir o espaço ocupado pela memória (ou não, se o tempo de execução alinhar os dados nos limites da memória nativa para um acesso mais rápido).
Lucero
4
A auto-implementação não é um ConcurrentHashSet, mas um ThreadSafeHashSet. Há uma grande diferença entre os 2 e é por isso que a Micorosft abandonou o SynchronizedCollections (as pessoas entenderam errado). Para serem "Concorrentes", operações como GetOrAdd, etc, devem ser implementadas (como o dicionário), caso contrário, a simultaneidade não pode ser garantida sem bloqueio adicional. Mas se você precisar de bloqueio adicional fora da classe, por que não usa um HashSet simples desde o início?
George Mavritsakis
36

Em vez de envolver ConcurrentDictionaryou bloquear um HashSet, criei um real com ConcurrentHashSetbase em ConcurrentDictionary.

Esta implementação suporta operações básicas por item sem HashSetas operações definidas, pois fazem menos sentido nos cenários simultâneos IMO:

var concurrentHashSet = new ConcurrentHashSet<string>(
    new[]
    {
        "hamster",
        "HAMster",
        "bar",
    },
    StringComparer.OrdinalIgnoreCase);

concurrentHashSet.TryRemove("foo");

if (concurrentHashSet.Contains("BAR"))
{
    Console.WriteLine(concurrentHashSet.Count);
}

Saída: 2

Você pode obtê-lo no NuGet aqui e ver a fonte no GitHub aqui .

i3arnon
fonte
3
Esta deve ser a resposta aceita, grande implantação
smirkingman
Add não deve ser renomeado para TryAdd para que seja consistente com ConcurrentDictionary ?
Neo
8
@ Neo Não ... porque está intencionalmente usando a semântica HashSet <T> , onde você chama Add e retorna um booleano indicando se o item foi adicionado (verdadeiro) ou se já existe (falso). msdn.microsoft.com/pt-br/library/bb353005(v=vs.110).aspx
G-Mac
Ele não deveria implementar a ISet<T>interface bo, na verdade, corresponde à HashSet<T>semântica?
Nekromancer 6/09
1
@Nekromancer, como eu disse na resposta, não acho que faz sentido fornecer esses métodos definidos em uma implementação simultânea. Overlapspor exemplo, seria necessário bloquear a instância durante toda a execução ou fornecer uma resposta que já pode estar errada. Ambas as opções são IMO ruins (e podem ser adicionadas externamente pelos consumidores).
I3arnon 6/09/18
21

Como ninguém mais o mencionou, vou oferecer uma abordagem alternativa que pode ou não ser apropriada para seu objetivo específico:

Coleções imutáveis ​​da Microsoft

De um post da equipe da MS por trás:

Embora criar e executar simultaneamente seja mais fácil do que nunca, um dos problemas fundamentais ainda existe: estado compartilhado mutável. A leitura de vários threads geralmente é muito fácil, mas uma vez que o estado precisa ser atualizado, fica muito mais difícil, especialmente em projetos que exigem bloqueio.

Uma alternativa ao bloqueio é fazer uso do estado imutável. É garantido que as estruturas de dados imutáveis ​​nunca mudam e, portanto, podem ser passadas livremente entre diferentes threads, sem se preocupar em pisar no pé de outra pessoa.

Esse design cria um novo problema: Como você gerencia alterações de estado sem copiar todo o estado de cada vez? Isso é especialmente complicado quando há coleções envolvidas.

É aqui que entram as coleções imutáveis.

Essas coleções incluem ImmutableHashSet <T> e ImmutableList <T> .

atuação

Como as coleções imutáveis ​​usam estruturas de dados em árvore abaixo para permitir o compartilhamento estrutural, suas características de desempenho são diferentes das coleções mutáveis. Ao comparar com uma coleção mutável de bloqueio, os resultados dependerão da contenção de bloqueio e dos padrões de acesso. No entanto, retirado de outra postagem no blog sobre as coleções imutáveis:

P: Ouvi dizer que coleções imutáveis ​​são lentas. Estes são diferentes? Posso usá-los quando o desempenho ou a memória são importantes?

R: Essas coleções imutáveis ​​foram altamente ajustadas para oferecer características de desempenho competitivas às coleções mutáveis ​​enquanto equilibram o compartilhamento de memória. Em alguns casos, elas são quase tão rápidas quanto as coleções mutáveis, tanto algoritmicamente quanto em tempo real, às vezes até mais rápidas, enquanto em outros casos são algoritmicamente mais complexas. Em muitos casos, porém, a diferença será insignificante. Geralmente você deve usar o código mais simples para realizar o trabalho e ajustar o desempenho conforme necessário. As coleções imutáveis ​​ajudam a escrever um código simples, especialmente quando a segurança de threads deve ser considerada.

Em outras palavras, em muitos casos, a diferença não será perceptível e você deve optar por uma opção mais simples - que para conjuntos simultâneos seria usada ImmutableHashSet<T>, já que você não possui uma implementação mutável de bloqueio! :-)

Søren Boisen
fonte
1
ImmutableHashSet<T>não ajuda muito se sua intenção é atualizar o estado compartilhado de vários threads ou estou faltando alguma coisa aqui?
Tugberk 26/05/19
7
@tugberk Sim e não. Como o conjunto é imutável, você terá que atualizar a referência, o que a coleção em si não ajuda. A boa notícia é que você reduziu o problema complexo de atualizar uma estrutura de dados compartilhada de vários threads para o problema muito mais simples de atualizar uma referência compartilhada. A biblioteca fornece o método ImmutableInterlocked.Update para ajudá-lo.
Søren Boisen
1
@ SørenBoisenjust leu sobre coleções imutáveis ​​e tentou descobrir como usá-las com segurança. ImmutableInterlocked.Updateparece ser o elo que faltava. Obrigado!
Xneg 28/09/19
4

A parte complicada de fazer uma ISet<T> concorrente é que os métodos definidos (união, interseção, diferença) são de natureza iterativa. No mínimo, você precisa iterar todos os n membros de um dos conjuntos envolvidos na operação, enquanto bloqueia os dois conjuntos.

Você perde as vantagens de ConcurrentDictionary<T,byte>quando precisa bloquear todo o conjunto durante a iteração. Sem travamento, essas operações não são seguras para threads.

Dada a sobrecarga adicional de ConcurrentDictionary<T,byte>, provavelmente é mais sensato usar o peso mais leveHashSet<T> e envolver tudo em travas.

Se você não precisar das operações definidas, use ConcurrentDictionary<T,byte> e use apenas default(byte)como o valor ao adicionar chaves.

pugby
fonte
2

Eu prefiro soluções completas, então fiz isso: lembre-se de que meu Count é implementado de uma maneira diferente, porque não vejo por que alguém deveria ser proibido de ler o hashset enquanto tentava contar seus valores.

@ Zen, Obrigado por começar.

[DebuggerDisplay("Count = {Count}")]
[Serializable]
public class ConcurrentHashSet<T> : ICollection<T>, ISet<T>, ISerializable, IDeserializationCallback
{
    private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

    private readonly HashSet<T> _hashSet = new HashSet<T>();

    public ConcurrentHashSet()
    {
    }

    public ConcurrentHashSet(IEqualityComparer<T> comparer)
    {
        _hashSet = new HashSet<T>(comparer);
    }

    public ConcurrentHashSet(IEnumerable<T> collection)
    {
        _hashSet = new HashSet<T>(collection);
    }

    public ConcurrentHashSet(IEnumerable<T> collection, IEqualityComparer<T> comparer)
    {
        _hashSet = new HashSet<T>(collection, comparer);
    }

    protected ConcurrentHashSet(SerializationInfo info, StreamingContext context)
    {
        _hashSet = new HashSet<T>();

        // not sure about this one really...
        var iSerializable = _hashSet as ISerializable;
        iSerializable.GetObjectData(info, context);
    }

    #region Dispose

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
            if (_lock != null)
                _lock.Dispose();
    }

    public IEnumerator<T> GetEnumerator()
    {
        return _hashSet.GetEnumerator();
    }

    ~ConcurrentHashSet()
    {
        Dispose(false);
    }

    public void OnDeserialization(object sender)
    {
        _hashSet.OnDeserialization(sender);
    }

    public void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        _hashSet.GetObjectData(info, context);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.Add(item);
        }
        finally
        {
            if(_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void UnionWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.UnionWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void IntersectWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.IntersectWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void ExceptWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        _lock.EnterReadLock();
        try
        {
            _hashSet.ExceptWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            if (_lock.IsReadLockHeld) _lock.ExitReadLock();
        }
    }

    public void SymmetricExceptWith(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.SymmetricExceptWith(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsSubsetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsSubsetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsSupersetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsSupersetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsProperSupersetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsProperSupersetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool IsProperSubsetOf(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.IsProperSubsetOf(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Overlaps(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Overlaps(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool SetEquals(IEnumerable<T> other)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.SetEquals(other);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    bool ISet<T>.Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Add(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.Clear();
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Contains(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        _lock.EnterWriteLock();
        try
        {
            _hashSet.CopyTo(array, arrayIndex);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        _lock.EnterWriteLock();
        try
        {
            return _hashSet.Remove(item);
        }
        finally
        {
            if (_lock.IsWriteLockHeld) _lock.ExitWriteLock();
        }
    }

    public int Count
    {
        get
        {
            _lock.EnterWriteLock();
            try
            {
                return _hashSet.Count;
            }
            finally
            {
                if(_lock.IsWriteLockHeld) _lock.ExitWriteLock();
            }

        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
}
Dbl
fonte
A trava é descartada ... mas e o hashset interno, quando sua memória é liberada?
David Rettenbacher
1
@Warappa é lançado após a coleta de lixo. A única vez em que anulo manualmente as coisas e apago toda a sua presença em uma classe é quando os assuntos contêm eventos e, portanto, PODEM vazar memória (como quando você usaria o ObservableCollection e seu evento alterado). Estou aberto a sugestões se você pode adicionar conhecimento ao meu entendimento sobre o assunto. Eu passei um par de dias pesquisando sobre coleta de lixo também e estou sempre curioso sobre novas informações
Dbl
@ AndreasMüller boa resposta, no entanto, eu estou querendo saber por que você está usando '_lock.EnterWriteLock ();' seguido por '_lock.EnterReadLock ();' em alguns métodos como 'IntersectWith', acho que não há necessidade de ler aqui, pois o bloqueio de gravação impedirá qualquer leitura quando inserida por padrão.
Jalal Disse
Se você sempre deve EnterWriteLock, por que EnterReadLockexiste? O bloqueio de leitura não pode ser usado para métodos como Contains?
ErikE
2
Este não é um ConcurrentHashSet, mas um ThreadSafeHashSet. Veja meu comentário na resposta @ZenLulz sobre a auto-implementação. Tenho 99% de certeza de que quem usou essas implementações terá um bug sério em seu aplicativo.
George Mavritsakis