HashSet <T> concurrente en .NET Framework?

151

Tengo la siguiente clase.

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

Necesito cambiar el campo "Datos" de diferentes subprocesos, por lo que me gustaría obtener algunas opiniones sobre mi implementación actual segura para subprocesos.

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 una mejor solución, ir directamente al campo y protegerlo del acceso concurrente por múltiples hilos?

kukab
fuente
¿Qué tal usar una de las colecciones bajoSystem.Collections.Concurrent
I4V
8
Por supuesto, hazlo privado.
Hans Passant
3
¡Desde una perspectiva de concurrencia, no veo mucho mal con lo que has hecho aparte del hecho de que el campo Datos sea público! Podría obtener un mejor rendimiento de lectura con ReaderWriterLockSlim si eso es un problema. msdn.microsoft.com/en-us/library/…
Allan Elder
@AllanElder ReaderWriterLockserá útil (eficiente) cuando haya varios lectores y un solo escritor. Tenemos que saber si este es el caso de OP
Sriram Sakthivel
2
La implementación actual no es realmente 'concurrente' :) Solo es segura para subprocesos.
indefinido el

Respuestas:

164

Su implementación es correcta. Desafortunadamente, .NET Framework no proporciona un tipo de hashset concurrente incorporado. Sin embargo, hay algunas soluciones alternativas.

ConcurrentDictionary (recomendado)

El primero es usar la clase ConcurrentDictionary<TKey, TValue>en el espacio de nombres System.Collections.Concurrent. En el caso, el valor no tiene sentido, por lo que podemos usar un simple byte(1 byte en memoria).

private ConcurrentDictionary<string, byte> _data;

Esta es la opción recomendada porque el tipo es seguro para subprocesos y le brinda las mismas ventajas que una HashSet<T>clave, excepto que el valor son objetos diferentes.

Fuente: Social MSDN

Bolsa concurrente

Si no le importan las entradas duplicadas, puede usar la clase ConcurrentBag<T>en el mismo espacio de nombres de la clase anterior.

private ConcurrentBag<string> _data;

Auto-implementación

Finalmente, como lo hizo, puede implementar su propio tipo de datos, utilizando el bloqueo u otras formas en que .NET le brinda seguridad para subprocesos. Aquí hay un gran ejemplo: Cómo implementar ConcurrentHashSet en .Net

El único inconveniente de esta solución es que el tipo HashSet<T>no tiene acceso oficialmente concurrente, incluso para operaciones de lectura.

Cito el código de la publicación vinculada (originalmente escrita 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
    }
}

EDITAR: Mueva los métodos de bloqueo de entrada fuera de los trybloques, ya que podrían lanzar una excepción y ejecutar las instrucciones contenidas en los finallybloques.

ZenLulz
fuente
8
un diccionario con valores basura es una lista
Ralf
44
@Ralf Bueno, es un conjunto, no una lista, ya que no está ordenado.
Servy
11
De acuerdo con el documento bastante breve de MSDN sobre "Colecciones y sincronización (seguridad de subprocesos)" , las clases en el sistema. Las subprocesos y los espacios de nombres relacionados pueden leerse de manera segura en varios subprocesos. Esto significa que HashSet puede leerse de manera segura mediante múltiples hilos.
Hank Schultz
77
@Oliver, una referencia usa mucha más memoria por entrada, incluso si es una nullreferencia (la referencia necesita 4 bytes en un tiempo de ejecución de 32 bits y 8 bytes en un tiempo de ejecución de 64 bits). Por lo tanto, el uso de a byte, una estructura vacía o similar puede reducir la huella de la memoria (o no si el tiempo de ejecución alinea los datos en los límites de la memoria nativa para un acceso más rápido).
Lucero
44
La auto implementación no es un ConcurrentHashSet sino un ThreadSafeHashSet. Hay una gran diferencia entre esos 2 y es por eso que Micorosft abandonó las Colecciones Sincronizadas (la gente se equivocó). Para que se realicen operaciones "concurrentes" como GetOrAdd, etc. (como el diccionario) o no se puede garantizar la concurrencia sin un bloqueo adicional. Pero si necesita un bloqueo adicional fuera de la clase, ¿por qué no utiliza un HashSet simple desde el principio?
George Mavritsakis
36

En lugar de envolver ConcurrentDictionaryo bloquear una HashSet, creé un realConcurrentHashSet basado en ConcurrentDictionary.

Esta implementación admite operaciones básicas por elemento sin HashSetlas operaciones establecidas, ya que tienen menos sentido en escenarios concurrentes IMO:

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

concurrentHashSet.TryRemove("foo");

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

Salida: 2

Puede obtenerlo de NuGet aquí y ver la fuente en GitHub aquí .

i3arnon
fuente
3
Esta debería ser la respuesta aceptada, gran implementación
smirkingman
No debe Agregar a llamarse a TryAdd por lo que es consistente con ConcurrentDictionary ?
Neo
8
@Neo No ... porque está utilizando intencionalmente la semántica HashSet <T> , donde se llama Agregar y devuelve un valor booleano que indica si el elemento se agregó (verdadero) o si ya existió (falso). msdn.microsoft.com/en-us/library/bb353005(v=vs.110).aspx
G-Mac
¿No debería implementar una ISet<T>interfaz bo que realmente coincida con la HashSet<T>semántica?
Nekromancer
1
@Nekromancer, como dije en la respuesta, no creo que tenga sentido proporcionar estos métodos establecidos en una implementación simultánea. Overlapspor ejemplo, necesitaría bloquear la instancia a lo largo de su ejecución o proporcionar una respuesta que ya puede estar equivocada. Ambas opciones son IMO malas (y los consumidores pueden agregarlas externamente).
i3arnon
21

Como nadie más lo mencionó, ofreceré un enfoque alternativo que puede o no ser apropiado para su propósito particular:

Colecciones inmutables de Microsoft

De una publicación de blog del equipo de MS detrás:

Si bien crear y ejecutar simultáneamente es más fácil que nunca, uno de los problemas fundamentales aún existe: el estado compartido mutable. Por lo general, leer de varios subprocesos es muy fácil, pero una vez que se necesita actualizar el estado, se vuelve mucho más difícil, especialmente en los diseños que requieren bloqueo.

Una alternativa al bloqueo es hacer uso del estado inmutable. Se garantiza que las estructuras de datos inmutables nunca cambiarán y, por lo tanto, se pueden pasar libremente entre diferentes hilos sin preocuparse de pisar los dedos de los pies de otra persona.

Sin embargo, este diseño crea un nuevo problema: ¿cómo se gestionan los cambios de estado sin copiar todo el estado cada vez? Esto es especialmente complicado cuando se trata de colecciones.

Aquí es donde entran las colecciones inmutables.

Estas colecciones incluyen ImmutableHashSet <T> e ImmutableList <T> .

Actuación

Dado que las colecciones inmutables utilizan estructuras de datos de árbol debajo para permitir el intercambio estructural, sus características de rendimiento son diferentes de las colecciones mutables. Cuando se compara con una colección mutable de bloqueo, los resultados dependerán de la contención de bloqueo y los patrones de acceso. Sin embargo, tomado de otra publicación de blog sobre las colecciones inmutables:

P: He escuchado que las colecciones inmutables son lentas. ¿Son estos diferentes? ¿Puedo usarlos cuando el rendimiento o la memoria es importante?

R: Estas colecciones inmutables se han ajustado para tener características de rendimiento competitivas con respecto a las colecciones mutables, al tiempo que se equilibra el intercambio de memoria. En algunos casos son casi tan rápidos como las colecciones mutables tanto algorítmicamente como en tiempo real, a veces incluso más rápido, mientras que en otros casos son algorítmicamente más complejos. Sin embargo, en muchos casos la diferencia será insignificante. En general, debe usar el código más simple para hacer el trabajo y luego ajustar el rendimiento según sea necesario. Las colecciones inmutables lo ayudan a escribir código simple, especialmente cuando se debe considerar la seguridad de subprocesos.

En otras palabras, en muchos casos la diferencia no será notable y debe elegir la opción más simple, que sería para conjuntos concurrentes ImmutableHashSet<T>, ¡ya que no tiene una implementación mutable de bloqueo existente! :-)

Søren Boisen
fuente
1
ImmutableHashSet<T>no ayuda mucho si su intención es actualizar el estado compartido de varios subprocesos o me falta algo aquí?
tugberk
77
@tugberk Sí y no. Dado que el conjunto es inmutable, deberá actualizar la referencia, que la colección en sí no le ayuda. La buena noticia es que ha reducido el complejo problema de actualizar una estructura de datos compartidos de múltiples subprocesos al problema mucho más simple de actualizar una referencia compartida. La biblioteca le proporciona el método ImmutableInterlocked.Update para ayudarlo con eso.
Søren Boisen
1
@ SørenBoisen leyó sobre colecciones inmutables y trató de descubrir cómo usarlas con seguridad para subprocesos. ImmutableInterlocked.UpdateParece ser el eslabón perdido. ¡Gracias!
xneg
4

La parte difícil de hacer un ISet<T> concurrente es que los métodos establecidos (unión, intersección, diferencia) son de naturaleza iterativa. Como mínimo, debe iterar sobre todos los n miembros de uno de los conjuntos involucrados en la operación, mientras bloquea ambos conjuntos.

Pierde las ventajas de un ConcurrentDictionary<T,byte>cuando tiene que bloquear todo el conjunto durante la iteración. Sin bloqueo, estas operaciones no son seguras para subprocesos.

Dada la sobrecarga adicional de ConcurrentDictionary<T,byte>, probablemente sea más sabio usar el peso más livianoHashSet<T> y simplemente rodear todo en las cerraduras.

Si no necesita las operaciones de configuración, use ConcurrentDictionary<T,byte>y solo use default(byte)como valor cuando agregue claves.

pugby
fuente
2

Prefiero soluciones completas, así que hice esto: tenga en cuenta que mi cuenta se implementa de una manera diferente porque no veo por qué uno debería tener prohibido leer el hashset al intentar contar sus valores.

@ Zen, gracias por empezar.

[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
fuente
El bloqueo se elimina ... pero ¿qué pasa con el hashset interno, cuando se libera su memoria?
David Rettenbacher
1
@Warappa se lanza tras la recolección de basura. La única vez que anulo manualmente las cosas y borro toda su presencia dentro de una clase es cuando los sujetos contienen eventos y, por lo tanto, PUEDEN perder memoria (como cuando usarías ObservableCollection y su evento cambiado). Estoy abierto a sugerencias si puede agregar conocimiento a mi comprensión sobre el tema. He pasado un par de días que investigan en la recolección de basura demasiado y siempre estoy curioso acerca de la nueva información
Dbl
@ AndreasMüller buena respuesta, sin embargo, me pregunto por qué está usando '_lock.EnterWriteLock ();' seguido de '_lock.EnterReadLock ();' en algunos métodos como 'IntersectWith', creo que no hay necesidad de leer aquí, ya que el bloqueo de escritura evitará cualquier lectura cuando se ingrese por defecto.
Jalal dijo el
Si siempre debes EnterWriteLock, ¿por qué EnterReadLockexiste? ¿No se puede usar el bloqueo de lectura para métodos como Contains?
ErikE
2
Este no es un ConcurrentHashSet sino un ThreadSafeHashSet. Vea mi comentario sobre la respuesta de @ZenLulz con respecto a la auto implementación. Estoy 99% seguro de que cualquiera que haya usado esas implementaciones tendrá un error grave en su aplicación.
George Mavritsakis