210 votes

HashSet<T> concourant dans .NET Framework ?

J'ai la classe suivante.

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

J'ai besoin de modifier le champ "Data" à partir de différents threads, et j'aimerais donc avoir des avis sur mon implémentation thread-safe actuelle.

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 consisterait à passer directement au champ et à le protéger contre l'accès simultané de plusieurs threads ?

234voto

ZenLulz Points 1126

Votre mise en œuvre est correcte. Malheureusement, le cadre de travail .NET ne fournit pas de type de hashset concurrent intégré. Il existe toutefois 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;

C'est l'option recommandée car le type est sûr pour les threads et offre les mêmes avantages que le type HashSet<T> sauf que la clé et la valeur sont des objets différents.

Source : Social MSDN

Auto-application

Enfin, comme vous l'avez fait, vous pouvez mettre en œuvre votre propre type de données, en utilisant des verrous ou d'autres moyens que .NET met à votre disposition pour être à l'abri 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 permet pas officiellement l'accès simultané, même pour les opérations de lecture.

Je cite le code de l'article lié (écrit à l'origine par 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 : Déplacer les méthodes de verrouillage de l'entrée à l'extérieur de la try car ils pourraient lever une exception et exécuter les instructions contenues dans le bloc finally blocs.

ConcurrentBag (déconseillé)

L'utilisation de ConcurrentBag<T> n'est pas conseillé, car ce type permet uniquement d'insérer un élément donné et de retirer un élément aléatoire de manière sûre pour les threads. Cette classe est conçue pour faciliter les scénarios producteur-consommateur, ce qui n'est pas l'objectif de l'OP (plus d'explications aquí ).

Les autres opérations (par exemple, celles fournies par les méthodes d'extension) font pas supporter une utilisation simultanée. La documentation MSDN met en garde : " Tous les membres publics et protégés de ConcurrentBag sont à l'abri des threads et peuvent être utilisés simultanément à partir de plusieurs threads. Cependant, les membres accessibles via l'une des interfaces mises en œuvre par ConcurrentBag, y compris les méthodes d'extension, ne sont pas garantis comme étant à l'abri des threads et peuvent nécessiter une synchronisation de la part de l'appelant. "

60voto

I3arnon Points 9498

Au lieu d'envelopper un ConcurrentDictionary ou le verrouillage sur un HashSet J'ai créé un véritable ConcurrentHashSet sur la base de ConcurrentDictionary .

Cette mise en œuvre permet d'effectuer des opérations de base par article sans HashSet car elles ont moins de sens dans les scénarios simultanés :

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 à partir de NuGet aquí et voir les sources sur GitHub aquí .

30voto

Søren Boisen Points 88

Puisque personne d'autre ne l'a mentionné, je vais proposer une approche alternative qui peut ou non être appropriée à votre objectif particulier :

Collections immuables de Microsoft

A partir d'un article de blog par l'équipe de l'EM derrière :

Si la création et l'exécution simultanées sont plus faciles que jamais, l'un des problèmes fondamentaux subsiste : l'état partagé mutable. La lecture à partir 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 qui nécessitent un verrouillage.

Une alternative au verrouillage est l'utilisation d'un état immuable. Les structures de données immuables sont garanties de ne jamais changer et peuvent donc être transmises librement entre différents threads sans craindre de marcher sur les plates-bandes 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 ? Ce problème est particulièrement délicat lorsqu'il s'agit de collections.

C'est là qu'interviennent les collections immuables.

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

Performance

Étant donné que les collections immuables utilisent des structures de données arborescentes 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épendront de la contention des verrous et des schémas d'accès. Toutefois, d'après un autre billet de blog sur les collections immuables :

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

R : Ces collections immuables ont été hautement optimisées pour présenter des caractéristiques de performance compétitives par rapport 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 qu'en temps réel, parfois même plus rapides, alors que dans d'autres cas, elles sont algorithmiquement plus complexes. Dans d'autres cas, elles sont plus complexes sur le plan algorithmique. Dans de nombreux cas, cependant, la différence est négligeable. En règle générale, il convient d'utiliser le code le plus simple pour effectuer le travail, puis d'ajuster 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 devriez opter pour le choix le plus simple, à savoir, pour les ensembles simultanés, utiliser ImmutableHashSet<T> puisque vous n'avez pas d'implémentation de verrouillage mutable existante ! :-)

4voto

pugby Points 41

La partie délicate de l'élaboration d'un ISet<T> est que les méthodes ensemblistes (union, intersection, différence) sont itératives par nature. Au minimum, vous devez itérer sur les n membres de l'un des ensembles concernés par l'opération, tout en verrouillant les deux ensembles.

Vous perdez les avantages d'un ConcurrentDictionary<T,byte> lorsque vous devez verrouiller l'ensemble des données pendant l'itération. Sans verrouillage, ces opérations ne sont pas sûres.

Compte tenu des frais généraux supplémentaires de ConcurrentDictionary<T,byte> il est probablement plus sage d'utiliser le poids le plus léger. HashSet<T> et entourer le tout de serrures.

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

2voto

Andreas Müller Points 673

Je préfère les solutions complètes, c'est donc ce que j'ai fait : Attention, mon Count est implémenté d'une manière différente car je ne vois pas pourquoi il serait interdit 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; }
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X