HashSet simultaneo <T> in .NET Framework?


152

Ho la seguente classe.

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

Devo cambiare il campo "Dati" da thread diversi, quindi vorrei alcune opinioni sulla mia attuale implementazione thread-safe.

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);
    }
}

Esiste una soluzione migliore, andare direttamente in campo e proteggerlo dall'accesso simultaneo di più thread?


Che ne dici di usare una delle collezioni sottoSystem.Collections.Concurrent
I4V

8
Certo, rendilo privato.
Hans Passant,

3
Dal punto di vista della concorrenza, non vedo molto di sbagliato in ciò che hai fatto se non che il campo Dati è pubblico! È possibile ottenere prestazioni di lettura migliori utilizzando ReaderWriterLockSlim se questo è un problema. msdn.microsoft.com/en-us/library/…
Allan Elder,

@AllanElder ReaderWriterLocksarà utile (efficiente) quando più lettori e un singolo autore. Dobbiamo sapere se è questo il caso di OP
Sriram Sakthivel,

2
L'implementazione attuale non è realmente "concorrente" :) È solo sicura per i thread.
non definito

Risposte:


165

L'implementazione è corretta. Purtroppo, .NET Framework non fornisce un tipo di hashset simultaneo incorporato. Tuttavia, ci sono alcune soluzioni alternative.

ConcurrentDictionary (consigliato)

Il primo è usare la classe ConcurrentDictionary<TKey, TValue>nello spazio dei nomi System.Collections.Concurrent. Nel caso, il valore è inutile, quindi possiamo usare un semplice byte(1 byte in memoria).

private ConcurrentDictionary<string, byte> _data;

Questa è l'opzione consigliata perché il tipo è thread-safe e offre gli stessi vantaggi di una HashSet<T>chiave tranne e il valore sono oggetti diversi.

Fonte: Social MSDN

ConcurrentBag

Se non ti dispiace per le voci duplicate, puoi usare la classe ConcurrentBag<T>nello stesso spazio dei nomi della classe precedente.

private ConcurrentBag<string> _data;

Self-implementazione

Infine, come hai fatto, puoi implementare il tuo tipo di dati, usando il blocco o altri modi che .NET ti fornisce per essere thread-safe. Ecco un ottimo esempio: come implementare ConcurrentHashSet in .Net

L'unico inconveniente di questa soluzione è che il tipo HashSet<T>non ha ufficialmente un accesso simultaneo, anche per le operazioni di lettura.

Cito il codice del post collegato (originariamente scritto da 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: sposta i metodi di blocco dell'entrata fuori dai tryblocchi, poiché potrebbero generare un'eccezione ed eseguire le istruzioni contenute nei finallyblocchi.


8
un dizionario con valori spazzatura è un elenco
Ralf

44
@Ralf Bene, è un set, non un elenco, in quanto non ordinato.
Servito il

11
Secondo il documento piuttosto breve di MSDN su "Raccolte e sincronizzazione (sicurezza dei thread)" , le classi in System.Collections e gli spazi dei nomi correlati possono essere lette in modo sicuro da più thread. Ciò significa che HashSet può essere letto in modo sicuro da più thread.
Hank Schultz,

7
@Oliver, un riferimento utilizza molta più memoria per voce, anche se è un nullriferimento (il riferimento richiede 4 byte in un runtime a 32 bit e 8 byte in un runtime a 64 bit). Pertanto, l'utilizzo di a byte, una struttura vuota o simile può ridurre il footprint della memoria (oppure potrebbe non avvenire se il runtime allinea i dati sui limiti della memoria nativa per un accesso più rapido).
Lucero,

4
L'autoimplementazione non è un ConcurrentHashSet ma piuttosto un ThreadSafeHashSet. C'è una grande differenza tra questi 2 ed è per questo che Micorosft ha abbandonato SynchronizedCollections (la gente ha sbagliato). Per essere operazioni "simultanee" come GetOrAdd ecc. Devono essere implementate (come il dizionario), altrimenti la concorrenza non può essere garantita senza ulteriore blocco. Ma se hai bisogno di un blocco aggiuntivo al di fuori della classe, perché non utilizzare un semplice HashSet fin dall'inizio?
George Mavritsakis,

36

Invece di avvolgere un ConcurrentDictionaryo bloccare su un HashSetho creato un effettivo ConcurrentHashSetbasato su ConcurrentDictionary.

Questa implementazione supporta le operazioni di base per articolo senza HashSetle operazioni impostate in quanto hanno meno senso in scenari simultanei IMO:

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

concurrentHashSet.TryRemove("foo");

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

Uscita: 2

Puoi scaricarlo da NuGet qui e vedere la fonte su GitHub qui .


3
Questa dovrebbe essere la risposta accettata, grande attuazione
smirkingman

Aggiungere non dovrebbe essere rinominato in TryAdd in modo che sia coerente con ConcurrentDictionary ?
Neo,

8
@Neo No ... perché utilizza intenzionalmente la semantica di HashSet <T> , dove si chiama Aggiungi e restituisce un valore booleano che indica se l'elemento è stato aggiunto (vero) o se esiste già (falso). msdn.microsoft.com/en-us/library/bb353005(v=vs.110).aspx
G-Mac

Non dovrebbe implementare l' ISet<T>interfaccia bo in realtà corrispondere alla HashSet<T>semantica?
Nekromancer,

1
@Nekromancer come ho detto nella risposta, non credo abbia senso fornire questi metodi impostati in un'implementazione concorrente. Overlapsad esempio, dovrebbe bloccare l'istanza per tutta la sua esecuzione o fornire una risposta che potrebbe già essere errata. Entrambe le opzioni sono cattive IMO (e possono essere aggiunte esternamente dai consumatori).
i3arnon,

21

Dal momento che nessuno lo ha menzionato, offrirò un approccio alternativo che potrebbe essere o non essere appropriato per il tuo scopo particolare:

Collezioni Microsoft Immutable

Da un post sul blog del team MS dietro:

Mentre la creazione e l'esecuzione simultanea è più semplice che mai, esiste ancora uno dei problemi fondamentali: lo stato condiviso mutabile. La lettura da più thread è in genere molto semplice, ma una volta che lo stato deve essere aggiornato, diventa molto più difficile, specialmente nei progetti che richiedono il blocco.

Un'alternativa al blocco sta facendo uso dello stato immutabile. Le strutture di dati immutabili sono garantite per non cambiare mai e possono quindi essere passate liberamente tra thread diversi senza preoccuparsi di calpestare le dita dei piedi di qualcun altro.

Questo design crea tuttavia un nuovo problema: come si gestiscono i cambiamenti di stato senza copiare l'intero stato ogni volta? Ciò è particolarmente complicato quando sono coinvolte le raccolte.

È qui che arrivano le collezioni immutabili.

Queste raccolte includono ImmutableHashSet <T> e ImmutableList <T> .

Prestazione

Poiché le raccolte immutabili utilizzano le strutture dei dati degli alberi sottostanti per consentire la condivisione strutturale, le loro caratteristiche prestazionali sono diverse dalle raccolte mutabili. Quando si confronta con una raccolta mutabile bloccabile, i risultati dipenderanno dalla contesa tra i blocchi e dai modelli di accesso. Tuttavia, tratto da un altro post sul blog sulle raccolte immutabili:

D: Ho sentito che le raccolte immutabili sono lente. Sono diversi? Posso usarli quando le prestazioni o la memoria sono importanti?

A: Queste raccolte immutabili sono state ottimizzate per avere caratteristiche di prestazioni competitive rispetto alle collezioni mutabili, bilanciando al contempo la condivisione della memoria. In alcuni casi sono quasi altrettanto veloci delle raccolte mutabili sia algoritmicamente che in tempo reale, a volte anche più velocemente, mentre in altri casi sono algoritmicamente più complesse. In molti casi, tuttavia, la differenza sarà trascurabile. Generalmente è necessario utilizzare il codice più semplice per completare il lavoro e quindi ottimizzare le prestazioni secondo necessità. Le raccolte immutabili ti aiutano a scrivere codice semplice, soprattutto quando si deve considerare la sicurezza dei thread.

In altre parole, in molti casi la differenza non sarà evidente e dovresti scegliere la scelta più semplice - che per i set simultanei sarebbe da usare ImmutableHashSet<T>, dal momento che non hai un'implementazione mutabile con blocco esistente! :-)


1
ImmutableHashSet<T>non aiuta molto se il tuo intento è quello di aggiornare lo stato condiviso da più thread o mi sto perdendo qualcosa qui?
Tugberk,

7
@tugberk Sì e no. Poiché il set è immutabile, dovrai aggiornare il riferimento ad esso, che la raccolta stessa non ti aiuta. La buona notizia è che hai ridotto il complesso problema dell'aggiornamento di una struttura di dati condivisa da più thread al problema molto più semplice dell'aggiornamento di un riferimento condiviso. La libreria ti fornisce il metodo ImmutableInterlocked.Update per aiutarti.
Søren Boisen,

1
@ SørenBoisenjust ha letto delle collezioni immutabili e ha cercato di capire come usarle in modo sicuro. ImmutableInterlocked.Updatesembra essere l'anello mancante. Grazie!
xneg,

4

La parte difficile della creazione di un ISet<T>concorrente è che i metodi impostati (unione, intersezione, differenza) sono di natura iterativa. Per lo meno, devi iterare su tutti i n membri di uno dei set coinvolti nell'operazione, bloccando entrambi i set.

Perdi i vantaggi di a ConcurrentDictionary<T,byte>quando devi bloccare l'intero set durante l'iterazione. Senza blocco, queste operazioni non sono thread-safe.

Dato il sovraccarico aggiunto di ConcurrentDictionary<T,byte>, è probabilmente più saggio solo usare il peso più leggero HashSet<T>e circondare tutto in serrature.

Se non hai bisogno delle operazioni impostate, usa ConcurrentDictionary<T,byte>e usa solo default(byte)come valore quando aggiungi le chiavi.


2

Preferisco soluzioni complete, quindi ho fatto questo: badate che il mio Count è implementato in modo diverso perché non vedo perché si dovrebbe vietare di leggere l'hashset mentre si tenta di contare i suoi valori.

@Zen, grazie per averlo iniziato.

[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; }
    }
}

La serratura viene eliminata ... ma per quanto riguarda l'hashset interno, quando viene rilasciata la sua memoria?
David Rettenbacher,

1
@Warappa viene rilasciato durante la raccolta dei rifiuti. L'unica volta che annulla manualmente le cose e cancella la loro intera presenza all'interno di una classe è quando i soggetti contengono eventi e quindi POSSONO perdere la memoria (come quando useresti ObservableCollection e il suo evento modificato). Sono aperto a suggerimenti se è possibile aggiungere conoscenza alla mia comprensione in merito all'argomento. Ho trascorso un paio di giorni a fare ricerche anche sulla raccolta dei rifiuti e sono sempre curioso di conoscere nuove informazioni
Doppio

@ AndreasMüller buona risposta, tuttavia mi chiedo perché stai usando '_lock.EnterWriteLock ();' seguito da '_lock.EnterReadLock ();' in alcuni metodi come 'IntersectWith' penso che non sia necessario l'aspetto di lettura qui poiché il blocco di scrittura impedirà qualsiasi lettura quando si accede per impostazione predefinita.
Jalal ha detto il

Se devi sempre EnterWriteLock, perché EnterReadLockesiste? Non è possibile utilizzare il blocco lettura per metodi come Contains?
ErikE,

2
Questo non è un ConcurrentHashSet ma piuttosto un ThreadSafeHashSet. Vedi il mio commento sulla risposta di @ZenLulz per quanto riguarda l'auto-implementazione. Sono sicuro al 99% che chiunque abbia utilizzato tali implementazioni avrà un grave bug nella propria applicazione.
George Mavritsakis,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.