Параллельный хэш-набор in.NET рамки?
у меня есть следующий класс.
class Test{
public HashSet<string> Data = new HashSet<string>();
}
мне нужно изменить поле "Данные" из разных потоков, поэтому я хотел бы получить некоторые мнения о моей текущей потокобезопасной реализации.
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);
}
}
есть ли лучшее решение, чтобы перейти непосредственно к полю и защитить его от параллельного доступа несколькими потоками?
5 ответов:
ваша реализация-это правильно. К сожалению, платформа .NET Framework не предоставляет встроенный параллельный тип hashset. Тем не менее, есть некоторые обходные пути.
ConcurrentDictionary (рекомендуется)
это первый, чтобы использовать класс
ConcurrentDictionary<TKey, TValue>
в пространстве именSystem.Collections.Concurrent
. В этом случае значение бессмысленно, поэтому мы можем использовать простойbyte
(1 байт памяти).private ConcurrentDictionary<string, byte> _data;
это рекомендуемый вариант, потому что тип потокобезопасный и предоставить вам те же преимущества, чем
HashSet<T>
кроме ключа и значения это разные объекты.источник: социальная MSDN
ConcurrentBag
если вы не возражаете против повторяющихся записей, вы можете использовать класс
ConcurrentBag<T>
в том же пространстве имен предыдущего класса.private ConcurrentBag<string> _data;
реализации
наконец, как и вы, вы можете реализовать свои собственные данные введите, используя блокировку или другие способы, которые .NET предоставляет вам для потокобезопасности. Вот отличный пример: как реализовать ConcurrentHashSet в .Net
единственным недостатком этого решения является то, что тип
HashSet<T>
официально не имеет параллельного доступа, даже для операций чтения.я цитирую код связанного сообщения (первоначально написанный Бен Мошер).
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: переместить входной замок методы, находящиеся не на
try
блоки, как они могли бы бросить исключение и выполнить инструкции, содержащиеся вfinally
блоки.
Вместо заключения
ConcurrentDictionary
или блокировка над aHashSet
Я создал фактическийConcurrentHashSet
на основеConcurrentDictionary
.эта реализация поддерживает основные операции для каждого элемента без
HashSet
операции набора, поскольку они имеют меньше смысла в параллельных сценариях IMO:var concurrentHashSet = new ConcurrentHashSet<string>( new[] { "hamster", "HAMster", "bar", }, StringComparer.OrdinalIgnoreCase); concurrentHashSet.TryRemove("foo"); if (concurrentHashSet.Contains("BAR")) { Console.WriteLine(concurrentHashSet.Count); }
выход: 2
вы можете получить его от NuGet здесь и смотрите источник на GitHub здесь.
поскольку никто больше не упоминал об этом, я предложу альтернативный подход, который может подходить или не подходить для конкретных целей:
Microsoft Неизменяемые Коллекции
С блоге командой MS позади:
хотя создание и одновременное выполнение проще, чем когда-либо, одна из фундаментальных проблем все еще существует: изменяемое общее состояние. Чтение из нескольких потоков, как правило, очень легко, но как только государство нуждается в обновлении, это становится намного сложнее, особенно в конструкциях, которые требуют блокировки.
альтернативой блокировке является использование неизменяемого состояния. Неизменяемые структуры данных гарантированно никогда не изменяются и, таким образом, могут свободно передаваться между различными потоками, не беспокоясь о наступлении на чужие пальцы.
этот дизайн создает новую проблему: как вы управляете изменениями в состоянии без копирования всего состояния каждый раз? Это особенно важно сложно, когда речь идет о коллекциях.
это где неизменяемые коллекции входят.
эти коллекции включают в себя ImmutableHashSet
и ImmutableList . производительность
поскольку неизменяемые коллекции используют древовидные структуры данных внизу для обеспечения структурного совместного использования, их характеристики производительности отличаются от изменяемых коллекций. При сравнении с блокировкой изменяемой коллекция, результаты будут зависеть от конкуренции блокировки и шаблонов доступа. Однако, взяты из еще один пост в блоге об immutable коллекции:
Q: я слышал, что immutable коллекции медленные. Это что-то другое? Могу ли я использовать их, когда важна производительность или память?
A: эти неизменяемые коллекции были высоко настроены, чтобы иметь конкурентоспособные характеристики производительности для изменяемых коллекций при балансировке памяти разделение. В некоторых случаях они почти так же быстры, как изменяемые коллекции как алгоритмически, так и в реальном времени, иногда даже быстрее, в то время как в других случаях они алгоритмически более сложны. Однако во многих случаях разница будет незначительной. Как правило, вы должны использовать самый простой код, чтобы выполнить работу, а затем настроить производительность по мере необходимости. Неизменяемые коллекции помогают вам писать простой код, особенно когда потокобезопасность должна быть продуманный.
другими словами, во многих случаях разница не будет заметна, и вы должны пойти с более простой выбор - что для параллельных наборов будет использовать
ImmutableHashSet<T>
, Так как у вас нет существующей блокировки изменяемой реализации! : -)
Я предпочитаю полные решения, поэтому я сделал это: имейте в виду, что мой счетчик реализован по-другому, потому что я не вижу, почему нужно запрещать читать хэш-набор при попытке подсчета его значений.
@Zen, Спасибо, что начали.
[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; } } }
хитрая часть о
ISet<T>
concurrent заключается в том, что методы набора (объединение, пересечение, разность) являются итерационными по своей природе. По крайней мере, вы должны перебирать все n членов одного из наборов, участвующих в операции, при блокировке обоих наборов.вы теряете преимущества a
ConcurrentDictionary<T,byte>
когда вы должны получить весь набор во время итерации. Без блокировки, эти операции не являются потокобезопасными.учитывая дополнительные накладные расходы
ConcurrentDictionary<T,byte>
, вероятно, разумнее просто использовать более легкий весHashSet<T>
и просто окружить все в замках.если вам не нужны операции набора, используйте
ConcurrentDictionary<T,byte>
и просто использоватьdefault(byte)
как значение при добавлении ключей.