HashSet Concorrente no .NET Framework?

Eu tenho a seguinte class.

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

Eu preciso alterar o campo “Data” de diferentes threads, então eu gostaria de algumas opiniões sobre a minha atual implementação thread-safe.

 class Test{ public HashSet Data = new HashSet(); 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 para o campo e protegê-lo do access simultâneo por vários segmentos?

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

    ConcurrentDictionary (recomendado)

    Este primeiro é usar a class ConcurrentDictionary no namespace System.Collections.Concurrent . No caso, o valor é inútil, então podemos usar um byte simples (1 byte na memory).

     private ConcurrentDictionary _data; 

    Essa é a opção recomendada porque o tipo é thread-safe e fornece as mesmas vantagens que um HashSet exceto que chave e valor são objects diferentes.

    Fonte: Social MSDN

    ConcurrentBag

    Se você não se importa com as inputs duplicadas, você pode usar a class ConcurrentBag no mesmo namespace da class anterior.

     private ConcurrentBag _data; 

    Auto-implementação

    Finalmente, como você fez, você pode implementar seu próprio tipo de dados, usando o bloqueio ou outras formas que o .NET fornece para você ser thread-safe. Aqui está um ótimo exemplo: Como implementar o ConcurrentHashSet no .Net

    A única desvantagem dessa solução é que o tipo HashSet não tem access oficialmente simultâneo, mesmo para operações de leitura.

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

     using System.Collections.Generic; using System.Threading; namespace BlahBlah.Utilities { public class ConcurrentHashSet : IDisposable { private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); private readonly HashSet _hashSet = new HashSet(); #region Implementation of ICollection ...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 methods de bloqueio de input para os blocos try , pois eles poderiam lançar uma exceção e executar as instruções contidas nos blocos finally .

    Em vez de agrupar um ConcurrentDictionary ou travar um HashSet , criei um ConcurrentHashSet real com base em ConcurrentDictionary .

    Esta implementação suporta operações básicas por item sem as operações de conjunto do HashSet , pois fazem menos sentido em cenários simultâneos IMO:

     var concurrentHashSet = new ConcurrentHashSet( new[] { "hamster", "HAMster", "bar", }, StringComparer.OrdinalIgnoreCase); concurrentHashSet.TryRemove("foo"); if (concurrentHashSet.Contains("BAR")) { Console.WriteLine(concurrentHashSet.Count); } 

    Saída: 2

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

    Como ninguém mais mencionou, vou oferecer uma abordagem alternativa que pode ou não ser apropriada para o seu propósito particular:

    Coleções imutáveis ​​da Microsoft

    De um post de blog da equipe do MS por trás:

    Embora a criação e a execução simultâneas sejam mais fáceis do que nunca, um dos problemas fundamentais ainda existe: estado compartilhado mutável. Ler a partir de vários threads é normalmente muito fácil, mas uma vez que o estado precisa ser atualizado, fica muito mais difícil, especialmente em projetos que exigem o bloqueio.

    Uma alternativa ao bloqueio é fazer uso do estado imutável. Estruturas de dados imutáveis ​​são garantidas para nunca mudar e, portanto, podem ser passadas livremente entre segmentos diferentes, sem se preocupar em pisar no pé de outra pessoa.

    Esse design cria um novo problema: Como você gerencia mudanças no estado sem copiar todo o estado a cada vez? Isso é especialmente complicado quando as collections estão envolvidas.

    É aqui que entram as collections imutáveis.

    Essas collections incluem ImmutableHashSet e ImmutableList .

    atuação

    Como as collections imutáveis ​​usam estruturas de dados de tree abaixo para permitir o compartilhamento estrutural, suas características de desempenho são diferentes das collections 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 access. No entanto, retirado de outro post do blog sobre as collections imutáveis:

    P: Ouvi dizer que collections imutáveis ​​são lentas. Estes são diferentes? Posso usá-los quando o desempenho ou a memory é importante?

    R: Essas collections imutáveis ​​foram altamente ajustadas para ter características de desempenho competitivas para as collections mutáveis ​​ao equilibrar o compartilhamento de memory. Em alguns casos, eles são quase tão rápidos quanto as collections mutáveis, tanto algoritmicamente quanto em tempo real, às vezes até mais rápido, enquanto em outros casos são algoritmicamente mais complexos. Em muitos casos, no entanto, a diferença será insignificante. Geralmente você deve usar o código mais simples para realizar o trabalho e, em seguida, ajustar o desempenho conforme necessário. As collections imutáveis ​​ajudam você a escrever código simples, especialmente quando a segurança de thread precisa ser considerada.

    Em outras palavras, em muitos casos, a diferença não será perceptível e você deve escolher a opção mais simples – que para conjuntos simultâneos seria usar ImmutableHashSet , já que você não tem uma implementação mutável de bloqueio existente! 🙂

    Eu prefiro soluções completas, então eu fiz isso: Lembre-se que meu Conde é implementado de uma maneira diferente porque eu não vejo porque 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 : ICollection, ISet, ISerializable, IDeserializationCallback { private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion); private readonly HashSet _hashSet = new HashSet(); public ConcurrentHashSet() { } public ConcurrentHashSet(IEqualityComparer comparer) { _hashSet = new HashSet(comparer); } public ConcurrentHashSet(IEnumerable collection) { _hashSet = new HashSet(collection); } public ConcurrentHashSet(IEnumerable collection, IEqualityComparer comparer) { _hashSet = new HashSet(collection, comparer); } protected ConcurrentHashSet(SerializationInfo info, StreamingContext context) { _hashSet = new HashSet(); // 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 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 other) { _lock.EnterWriteLock(); _lock.EnterReadLock(); try { _hashSet.UnionWith(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); if (_lock.IsReadLockHeld) _lock.ExitReadLock(); } } public void IntersectWith(IEnumerable other) { _lock.EnterWriteLock(); _lock.EnterReadLock(); try { _hashSet.IntersectWith(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); if (_lock.IsReadLockHeld) _lock.ExitReadLock(); } } public void ExceptWith(IEnumerable other) { _lock.EnterWriteLock(); _lock.EnterReadLock(); try { _hashSet.ExceptWith(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); if (_lock.IsReadLockHeld) _lock.ExitReadLock(); } } public void SymmetricExceptWith(IEnumerable other) { _lock.EnterWriteLock(); try { _hashSet.SymmetricExceptWith(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); } } public bool IsSubsetOf(IEnumerable other) { _lock.EnterWriteLock(); try { return _hashSet.IsSubsetOf(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); } } public bool IsSupersetOf(IEnumerable other) { _lock.EnterWriteLock(); try { return _hashSet.IsSupersetOf(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); } } public bool IsProperSupersetOf(IEnumerable other) { _lock.EnterWriteLock(); try { return _hashSet.IsProperSupersetOf(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); } } public bool IsProperSubsetOf(IEnumerable other) { _lock.EnterWriteLock(); try { return _hashSet.IsProperSubsetOf(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); } } public bool Overlaps(IEnumerable other) { _lock.EnterWriteLock(); try { return _hashSet.Overlaps(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); } } public bool SetEquals(IEnumerable other) { _lock.EnterWriteLock(); try { return _hashSet.SetEquals(other); } finally { if (_lock.IsWriteLockHeld) _lock.ExitWriteLock(); } } bool ISet.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; } } } 

    A parte complicada de se fazer um ISet simultâneo é que os methods set (union, intersection, difference) são de natureza iterativa. No mínimo, você tem que percorrer todos os n membros de um dos conjuntos envolvidos na operação, enquanto bloqueia os dois conjuntos.

    Você perde as vantagens de um ConcurrentDictionary quando tiver que bloquear todo o conjunto durante a iteração. Sem bloqueio, essas operações não são thread-safe.

    Dada a sobrecarga adicionada do ConcurrentDictionary , é provavelmente mais sábio usar o peso mais leve do HashSet e apenas cercar tudo nos bloqueios.

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