web-dev-qa-db-fra.com

HashSet simultané <T> dans .NET Framework?

J'ai la classe suivante.

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

Je dois modifier le champ "Données" à partir de différents threads. J'aimerais donc connaître votre avis sur la mise en œuvre actuelle de mon 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);
    }
}

Existe-t-il une meilleure solution, qui consiste à accéder directement à field et à le protéger contre les accès simultanés de plusieurs threads?

129
kukab

Votre implémentation est correcte. Le .NET Framework ne fournit malheureusement pas de type de hachage simultané intégré. Cependant, il existe des solutions de contournement.

ConcurrentDictionary (recommandé)

La première consiste à utiliser la classe ConcurrentDictionary<TKey, TValue> dans l'espace de noms System.Collections.Concurrent. Dans ce cas, la valeur est inutile, nous pouvons donc utiliser un simple byte (1 octet en mémoire).

private ConcurrentDictionary<string, byte> _data;

Cette option est recommandée car le type est thread-safe et vous offre les mêmes avantages qu'un HashSet<T> sauf que la clé et la valeur sont des objets différents.

Source: Social MSDN

ConcurrentBag

Si les entrées en double ne vous gênent pas, vous pouvez utiliser la classe ConcurrentBag<T> dans le même espace de nom que la classe précédente.

private ConcurrentBag<string> _data;

Auto-implémentation

Enfin, comme vous l'avez fait, vous pouvez implémenter votre propre type de données, en utilisant un verrou ou d'autres moyens que le .NET vous fournit pour la sécurité des threads. Voici un excellent exemple: Comment implémenter ConcurrentHashSet dans .Net

Le seul inconvénient de cette solution est que le type HashSet<T> ne constitue pas un accès officiellement concurrent, même pour les opérations de lecture.

Je cite le code de l'article lié (écrit à l'origine par Ben Mosher ).

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: Déplacez les méthodes de verrouillage d'entrée en dehors des blocs try, car elles pourraient lever une exception et exécuter les instructions contenues dans le finally blocs.

143
ZenLulz

Au lieu d'envelopper une ConcurrentDictionary ou de la verrouiller sur une HashSet, j'ai créé une réelle ConcurrentHashSet basée sur ConcurrentDictionary.

Cette implémentation prend en charge les opérations de base par élément sans les opérations définies de HashSet car elles sont moins utiles dans les scénarios concurrents. OMI:

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

concurrentHashSet.TryRemove("foo");

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

Sortie: 2

Vous pouvez l'obtenir auprès de NuGet ici et voir la source sur GitHub ici .

28
i3arnon

Puisque personne d'autre n'en a parlé, je proposerai une approche alternative qui pourrait ne pas convenir à votre objectif particulier:

Collections immuables Microsoft

D'un article de blog par l'équipe MS derrière:

Bien que la création et l’exécution simultanée soit plus facile que jamais, l’un des problèmes fondamentaux existe toujours: l’état partagé mutable. La lecture de plusieurs threads est généralement très facile, mais une fois que l'état doit être mis à jour, cela devient beaucoup plus difficile, en particulier dans les conceptions nécessitant un verrouillage.

Une alternative au verrouillage consiste à utiliser l'état immuable. Les structures de données immuables ont la garantie de ne jamais changer et peuvent donc être échangées librement entre différents threads sans se soucier de ne pas marcher sur les pieds de quelqu'un d'autre.

Cette conception crée cependant un nouveau problème: comment gérer les changements d'état sans copier l'état entier à chaque fois? Ceci est particulièrement difficile lorsque des collections sont impliquées.

C’est là que les collections immuables entrent en jeu.

Ces collections comprennent ImmutableHashSet <T> et ImmutableList <T> .

Performance

Comme les collections immuables utilisent des structures de données arborescentes sous-jacentes pour permettre le partage structurel, leurs caractéristiques de performance sont différentes de celles des collections mutables. Lors de la comparaison avec une collection mutable verrouillable, les résultats dépendent des conflits de verrouillage et des modèles d'accès. Cependant, tiré de autre article de blog à propos des collections immuables:

Q: J'ai entendu dire que les collections immuables sont lentes. Sont-ils différents? Puis-je les utiliser lorsque les performances ou la mémoire sont importantes?

R: Ces collections immuables ont été hautement ajustées pour offrir des performances compétitives aux collections mutables tout en équilibrant le partage de la mémoire. Dans certains cas, elles sont presque aussi rapides que les collections mutables, tant sur le plan algorithmique que dans le temps réel, parfois même plus rapidement, alors que dans d'autres cas, elles sont plus complexes sur le plan algorithmique. Dans de nombreux cas, toutefois, la différence sera négligeable. En règle générale, vous devez utiliser le code le plus simple pour exécuter le travail, puis optimiser les performances si nécessaire. Les collections immuables vous aident à écrire du code simple, en particulier lorsque la sécurité des threads doit être prise en compte.

En d'autres termes, dans de nombreux cas, la différence ne sera pas perceptible et vous devrez opter pour un choix plus simple: utiliser des ensembles concurrents (_ImmutableHashSet<T>_, car vous n'avez pas d'implémentation verrouillable verrouillable existante! :-)

19
Søren Boisen

Comme je préfère les solutions complètes, j’ai fait ceci: Remarquez que mon compte est mis en œuvre de manière différente car je ne vois pas pourquoi on devrait interdire de lire le hashset tout en essayant de compter ses valeurs.

@Zen, Merci d'avoir commencé.

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

La difficulté de la création d’un ISet<T> concurrent est que les méthodes définies (union, intersection, différence) sont de nature itérative. Au minimum, vous devez parcourir tous les n membres de l'un des ensembles impliqués dans l'opération tout en verrouillant les deux ensembles.

Vous perdez les avantages d'un ConcurrentDictionary<T,byte> lorsque vous devez verrouiller l'ensemble pendant l'itération. Sans verrouillage, ces opérations ne sont pas thread-safe.

Compte tenu de la surcharge ajoutée de ConcurrentDictionary<T,byte>, il est probablement plus sage d’utiliser simplement le poids plus léger HashSet<T> et de simplement entourer le tout de verrous.

Si vous n'avez pas besoin des opérations définies, utilisez ConcurrentDictionary<T,byte> et utilisez simplement default(byte) comme valeur lorsque vous ajoutez des clés.

3
pugby