Параллельный HashSet <T> в .NET Framework?

152

У меня есть следующий класс.

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);
    }
}

Есть ли лучшее решение, чтобы перейти непосредственно к полю и защитить его от одновременного доступа несколькими потоками?

kukab
источник
Как насчет использования одной из коллекций подSystem.Collections.Concurrent
I4V
8
Конечно, сделайте это частным.
Ганс Пассант
3
С точки зрения параллелизма, я не вижу ничего плохого в том, что вы сделали, кроме того, что поле данных было общедоступным! Вы можете улучшить производительность чтения, используя ReaderWriterLockSlim, если это вызывает озабоченность. msdn.microsoft.com/en-us/library/…
Аллан Элдер
@AllanElder ReaderWriterLockбудет полезен (эффективен), когда несколько читателей и один писатель. Мы должны знать, так ли это для ОП
Шрирам Шактивель
2
Текущая реализация на самом деле не «параллельная» :) Это просто потокобезопасно.
не определено

Ответы:

165

Ваша реализация верна. К сожалению, .NET Framework не предоставляет встроенный тип одновременного хэш-набора. Однако есть некоторые обходные пути.

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;
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
    }
}

РЕДАКТИРОВАТЬ: Переместите методы блокировки входа за пределы tryблоков, так как они могут вызвать исключение и выполнить инструкции, содержащиеся в finallyблоках.

ZenLulz
источник
8
словарь со значениями мусора является списком
Ralf
44
@Ralf Ну, это набор, а не список, так как он неупорядочен.
Servy
11
Согласно довольно короткому документу MSDN «Коллекции и синхронизация (безопасность потоков)» , классы в System.Collections и связанных пространствах имен могут безопасно считываться несколькими потоками. Это означает, что HashSet может быть безопасно прочитан несколькими потоками.
Хэнк Шульц
7
@Oliver, ссылка использует гораздо больше памяти для каждой записи, даже если это nullссылка (для ссылки требуется 4 байта в 32-битной среде выполнения и 8 байтов в 64-битной среде выполнения). Следовательно, использование byteпустой структуры или аналогичного может уменьшить объем занимаемой памяти (или не может быть, если среда выполнения выравнивает данные по собственным границам памяти для более быстрого доступа).
Лусеро
4
Самостоятельная реализация - это не ConcurrentHashSet, а ThreadSafeHashSet. Между этими двумя есть большая разница, и именно поэтому Micorosft отказался от SynchronizedCollections (люди ошиблись). Чтобы быть «Параллельными» операции, такие как GetOrAdd и т. Д. Должны быть реализованы (например, словарь), иначе параллелизм не может быть обеспечен без дополнительной блокировки. Но если вам нужна дополнительная блокировка вне класса, то почему бы вам не использовать простой HashSet с самого начала?
Джордж Маврицакис
36

Вместо того, чтобы обернуть ConcurrentDictionaryили заблокировать, HashSetя создал фактическое ConcurrentHashSetоснованное на ConcurrentDictionary.

Эта реализация поддерживает базовые операции для каждого элемента без HashSetопераций set, поскольку они имеют меньший смысл в параллельных сценариях 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 здесь .

i3arnon
источник
3
Это должно быть принято решение, большой реализация
smirkingman
Не следует ли переименовать Add в TryAdd, чтобы он соответствовал ConcurrentDictionary ?
Нео
8
@Neo Нет ... потому что он намеренно использует семантику HashSet <T> , где вы вызываете Add, и он возвращает логическое значение, указывающее, был ли элемент добавлен (true) или он уже существовал (false). msdn.microsoft.com/en-us/library/bb353005(v=vs.110).aspx
G-Mac
Разве он не должен реализовывать ISet<T>интерфейс bo на самом деле совпадает с HashSet<T>семантикой?
Некромант
1
@Nekromancer, как я сказал в ответе, я не думаю, что имеет смысл предоставлять эти методы набора в параллельной реализации. Overlapsнапример, нужно будет либо заблокировать экземпляр во время его выполнения, либо предоставить ответ, который уже может быть неправильным. Оба варианта плохие IMO (и могут быть добавлены внешне потребителями).
i3arnon
21

Поскольку никто другой не упомянул об этом, я предложу альтернативный подход, который может подходить или не подходить для вашей конкретной цели:

Неизменяемые коллекции Microsoft

Из сообщения в блоге команды MS позади:

Хотя создание и запуск одновременно проще, чем когда-либо, все еще существует одна из фундаментальных проблем: изменяемое общее состояние. Чтение из нескольких потоков обычно очень просто, но как только необходимо обновить состояние, становится намного сложнее, особенно в проектах, требующих блокировки.

Альтернативой блокировке является использование неизменного состояния. Неизменяемые структуры данных гарантированно никогда не изменятся и, таким образом, могут свободно передаваться между различными потоками, не беспокоясь о том, чтобы наступить на чужие пальцы.

Этот дизайн создает новую проблему: как управлять изменениями состояния, не копируя каждый раз все состояние? Это особенно сложно, когда участвуют коллекции.

Это где неизменные коллекции входят.

Эти коллекции включают ImmutableHashSet <T> и ImmutableList <T> .

Производительность

Поскольку неизменяемые коллекции используют древовидные структуры данных для обеспечения структурного совместного использования, их характеристики производительности отличаются от изменчивых коллекций. При сравнении с изменяемой блокировкой коллекции результаты будут зависеть от конкуренции за блокировку и шаблонов доступа. Однако взято из другого поста в блоге об неизменных коллекциях:

Q: Я слышал, что неизменные коллекции медленные. Это что-то другое? Могу ли я использовать их, когда важны производительность или память?

О: Эти неизменяемые коллекции были настроены так, чтобы иметь конкурентоспособные характеристики производительности с изменяемыми коллекциями при одновременном распределении памяти. В некоторых случаях они почти так же быстры, как изменяемые коллекции, как алгоритмически, так и в реальном времени, иногда даже быстрее, в то время как в других случаях они алгоритмически более сложны. Однако во многих случаях разница будет незначительной. Как правило, вы должны использовать самый простой код для выполнения работы, а затем настраивать производительность по мере необходимости. Неизменяемые коллекции помогают вам писать простой код, особенно когда нужно учитывать безопасность потоков.

Другими словами, во многих случаях разница не будет заметна, и вы должны пойти на более простой выбор - который для параллельных наборов будет использовать ImmutableHashSet<T>, поскольку у вас нет существующей реализации изменяемой блокировки! :-)

Сорен Бойсен
источник
1
ImmutableHashSet<T>не очень помогает, если вы собираетесь обновить общее состояние из нескольких потоков, или я что-то здесь упустил?
Тагберк
7
@tugberk Да и нет. Поскольку набор является неизменяемым, вам придется обновить ссылку на него, с которой сама коллекция вам не поможет. Хорошая новость заключается в том, что вы уменьшили сложную проблему обновления общей структуры данных из нескольких потоков до гораздо более простой проблемы обновления общей ссылки. Библиотека предоставляет вам метод ImmutableInterlocked.Update, чтобы помочь вам в этом.
Сорен Бойсен
1
@ SørenBoisenjust читал об неизменных коллекциях и пытался выяснить, как их использовать. ImmutableInterlocked.UpdateКажется, недостающее звено. Спасибо!
xneg
4

Сложность создания ISet<T>параллелизма заключается в том, что методы множества (объединение, пересечение, разность) по своей природе являются итеративными. По крайней мере, вам нужно перебрать все n членов одного из наборов, участвующих в операции, при этом блокируя оба набора.

Вы теряете преимущества, ConcurrentDictionary<T,byte>когда вам приходится блокировать весь набор во время итерации. Без блокировки эти операции не являются потокобезопасными.

Учитывая дополнительные накладные расходы ConcurrentDictionary<T,byte>, вероятно, разумнее всего использовать более легкий вес HashSet<T>и просто окружить все в замках.

Если вам не нужны операции set, используйте ConcurrentDictionary<T,byte>и просто используйте default(byte)в качестве значения при добавлении ключей.

pugby
источник
2

Я предпочитаю полные решения, поэтому я сделал это: имейте в виду, что мой счетчик реализован по-другому, потому что я не понимаю, почему нужно запретить читать хэш-набор при попытке подсчета его значений.

@ 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; }
    }
}
дублированный
источник
Блокировка удаляется ... но как насчет внутреннего хэш-набора, когда освобождается его память?
Дэвид Реттенбахер
1
@ Warappa выпущено после сборки мусора. Единственный раз, когда я вручную обнуляю вещи и очищаю их все присутствие в классе, это когда предметы содержат события и, таким образом, МОГУТ утекать память (например, когда вы используете ObservableCollection и его измененное событие). Я открыт для предложений, если вы можете добавить знания в моем понимании по этому вопросу. Я провел пару дней, изучая сборку мусора, и мне всегда любопытно узнать новую информацию
Dbl
@ AndreasMüller хороший ответ, однако мне интересно, почему вы используете «_lock.EnterWriteLock ();», а затем «_lock.EnterReadLock ();» в некоторых методах, таких как IntersectWith, я думаю, что здесь нет необходимости в чтении, так как блокировка записи предотвратит любое чтение (чтения) при вводе по умолчанию.
Джалал Саид
Если вы всегда должны EnterWriteLock, почему вообще EnterReadLockсуществует? Не может ли блокировка чтения использоваться для таких методов, как Contains?
ErikE
2
Это не ConcurrentHashSet, а ThreadSafeHashSet. Смотрите мой комментарий к ответу @ZenLulz относительно самореализации. Я на 99% уверен, что у любого, кто использовал эти реализации, будет серьезная ошибка в их приложении.
Джордж Маврицакис