HashSet concurrente dans .NET Framework?

J’ai le cours suivant.

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

J’ai besoin de changer le champ “Données” à partir de différents threads, donc j’aimerais avoir quelques opinions sur mon implémentation actuelle de thread-safe.

 class Test{ public HashSet Data = new HashSet(); public void Add(ssortingng Val){ lock(Data) Data.Add(Val); } public void Remove(ssortingng Val){ lock(Data) Data.Remove(Val); } } 

Existe-t-il une meilleure solution, pour accéder directement au champ et le protéger des access simultanés par plusieurs threads?

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

ConcurrentDictionary (recommandé)

Ce premier consiste à utiliser la classe ConcurrentDictionary dans l’espace de noms System.Collections.Concurrent . Dans le cas, la valeur est inutile, nous pouvons donc utiliser un simple byte (1 octet en mémoire).

 private ConcurrentDictionary _data; 

C’est l’option recommandée car le type est thread-safe et vous offre les mêmes avantages qu’un HashSet sauf que la clé et la valeur sont des objects différents.

Source: Social MSDN

ConcurrentBag

Si les entrées en double ne vous dérangent pas, vous pouvez utiliser la classe ConcurrentBag dans le même espace de noms de la classe précédente.

 private ConcurrentBag _data; 

Auto-implémentation

Enfin, comme vous l’avez fait, vous pouvez implémenter votre propre type de données, en utilisant lock ou d’autres moyens que .NET vous offre pour être thread-safe. Voici un excellent exemple: Comment implémenter ConcurrentHashSet dans .Net

Le seul inconvénient de cette solution est que le type HashSet n’accède pas officiellement aux access simultanés, même pour les opérations de lecture.

Je cite le code du message lié (écrit à l’origine par 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: Déplacer les méthodes de locking d’entrée en dehors des blocs try , car ils pourraient lancer une exception et exécuter les instructions contenues dans les blocs finally .

Au lieu d’encapsuler un ConcurrentDictionary ou de verrouiller un HashSet j’ai créé un véritable ConcurrentHashSet basé sur ConcurrentDictionary .

Cette implémentation prend en charge les opérations de base par élément sans les opérations définies par HashSet car elles sont moins HashSet dans les scénarios simultanés IMO:

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

Sortie: 2

Vous pouvez l’obtenir de NuGet ici et voir la source sur GitHub ici .

Étant donné que personne d’autre ne l’a mentionné, je proposerai une approche alternative qui pourrait ou non convenir à votre objective particulier:

Collections immuables Microsoft

À partir d’un article de blog de l’équipe MS derrière:

Bien que la création et l’exécution simultanées soient plus faciles 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, il devient beaucoup plus difficile, en particulier dans les conceptions nécessitant un locking.

Une alternative au locking utilise l’é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 se soucier de marcher sur les orteils 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 délicat lorsque des collections sont impliquées.

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

Ces collections incluent ImmutableHashSet et ImmutableList .

Performance

Comme les collections immuables utilisent des structures de données arborescentes en dessous pour permettre le partage structurel, leurs caractéristiques de performance sont différentes de celles des collections modifiables. Lorsque vous comparez à une collection verrouillable mutable, les résultats dépendront des conflits d’access et des modèles d’access. Cependant, tiré d’ un autre article sur les collections immuables:

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

R: Ces collections immuables ont été spécialement conçues pour offrir des performances compétitives aux collections modifiables tout en équilibrant le partage de la mémoire. Dans certains cas, ils sont presque aussi rapides que les collections mutables, à la fois par algorithme et en temps réel, parfois même plus rapidement, alors que dans d’autres cas, ils sont algorithmiquement plus complexes. Dans de nombreux cas, la différence sera toutefois négligeable. En règle générale, vous devez utiliser le code le plus simple pour effectuer le travail, puis 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 – qui pour les ensembles concurrents consisterait à utiliser ImmutableHashSet , car vous n’avez pas d’implémentation verrouillable existante! 🙂

Je préfère les solutions complètes, donc j’ai fait ceci: Mind my my Count est implémenté différemment car je ne vois pas pourquoi on devrait être interdit de lire le hashset en essayant de compter ses valeurs.

@Zen, Merci d’avoir commencé.

 [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 SymmesortingcExceptWith(IEnumerable other) { _lock.EnterWriteLock(); try { _hashSet.SymmesortingcExceptWith(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; } } } 

La ISet créer un ISet simultané 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 lorsque vous devez verrouiller l’ensemble au cours de l’itération. Sans locking, ces opérations ne sont pas thread-safe.

Compte tenu de la surcharge de ConcurrentDictionary , il est probablement préférable d’utiliser le HashSet plus léger, et d’entourer tout ce qui se trouve dans les verrous.

Si vous n’avez pas besoin des opérations set, utilisez ConcurrentDictionary et utilisez simplement default(byte) comme valeur lorsque vous ajoutez des clés.