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


151

У мене такий клас.

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

Чи є краще рішення - перейти безпосередньо до поля та захистити його від одночасного доступу декількома потоками?


Як щодо використання однієї з колекцій підSystem.Collections.Concurrent
— I4V

8
Звичайно, зробіть це приватним.
— Ганс Пасант

3
З точки зору одночасності, я не бачу нічого поганого в тому, що ви робили, окрім поля "Дані", яке є публічним! Ви можете покращити ефективність читання за допомогою ReaderWriterLockSlim, якщо це викликає занепокоєння. msdn.microsoft.com/en-us/library/…
— Аллан Старший

@AllanElder ReaderWriterLockбуде корисним (ефективним), коли кілька читачів і один автор. Ми маємо знати, чи так це для ОП
— Шрірам Сактівель

2
Поточна реалізація насправді не є "одночасною" :) Це просто безпечно для потоків.
— визначено

Відповіді:


164

Ваша реалізація правильна. На жаль, .NET Framework не пропонує вбудований паралельний тип хештету, на жаль. Однак є деякі шляхи вирішення.

Паралельний словник (рекомендовано)

Перший - використовувати клас ConcurrentDictionary<TKey, TValue>у просторі імен System.Collections.Concurrent. У випадку значення є безглуздим, тому ми можемо використовувати просте byte(1 байт в пам'яті).

private ConcurrentDictionary<string, byte> _data;

Це рекомендований варіант, оскільки тип захищений від потоку і забезпечує ті самі переваги, що HashSet<T>інакше, ніж ключ та значення - це різні об'єкти.

Джерело: Social 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блоках.


8
Словник зі значеннями небажаного - це список
— Ralf

44
@Ralf Ну, це набір, а не список, як це не упорядковано.
— Сервіс

11
Згідно з досить коротким документом MSDN на тему "Колекції та синхронізація (безпека нитки)" , класи в System.Collections та пов’язані з ними простори імен можна читати декількома потоками безпечно. Це означає, що HashSet можна сміливо читати декількома потоками.
— Hank Schultz

7
@Oliver, посилання використовує набагато більше пам’яті на запис, навіть якщо це nullпосилання (для посилання потрібно 4 байти в 32-бітному режимі виконання та 8 байт у 64-бітному режимі). Тому використання a byte, порожня структура або подібне може зменшити слід пам’яті (або не може, якщо час виконання вирівняє дані за межами нашої пам'яті для швидшого доступу).
— Lucero

4
Самореалізація - це не ConcurrentHashSet, а скоріше ThreadSafeHashSet. Існує велика різниця між цими 2, і саме тому Micorosft відмовився від SynchronizedCollections (люди зрозуміли це неправильно). Для того, щоб бути "паралельними" такі операції, як GetOrAdd тощо, слід реалізувати (як-от словник), інакше одночасність не може бути забезпечена без додаткового блокування. Але якщо вам потрібно додаткове блокування поза класом, то чому б ви не скористалися простим HashSet з самого початку?
— Джордж Маврицакіс

36

Замість того, щоб обгортати ConcurrentDictionaryабо заблокувати HashSetI, я створив фактичне 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 можна тут .


3
Це має бути прийнята відповідь, чудова реалізація
— smirkingman

Чи не слід додавати перейменований на TryAdd, щоб він відповідав ConcurrentDictionary ?
— Нео

8
@Neo Ні ... тому що навмисно використовується семантика HashSet <T> , де ви викликаєте Додати, і вона повертає булеве значення із зазначенням того, що елемент був доданий (true), чи він вже існував (false). msdn.microsoft.com/en-us/library/bb353005(v=vs.110).aspx
— G-Mac

Чи не слід реалізовувати ISet<T>інтерфейс, який фактично відповідає HashSet<T>семантиці?
— Некроманс

1
@Nekromancer, як я вже говорив у відповіді, я не думаю, що має сенс надавати ці задані методи у паралельній реалізації. Overlapsнаприклад, потрібно або заблокувати примірник протягом усього періоду його виконання, або надати відповідь, яка вже може бути помилковою. Обидва варіанти погані ІМО (і можуть бути додані споживачами зовні).
— i3arnon

21

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

Microsoft Immutable Collection

З публікації в блозі команди MS за блогом :

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

Альтернативою блокуванню є використання непорушного стану. Безперервні структури даних гарантуються, що вони ніколи не змінюватимуться, і тому вони можуть вільно передаватися між різними потоками, не переживаючи про наступання на чужі пальці.

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

Тут надходять непорушні колекції.

Ці колекції включають ImmutableHashSet <T> і ImmutableList <T> .

Продуктивність

Оскільки в незмінних колекціях використовуються структури даних дерев під ними, щоб забезпечити спільний доступ до структур, характеристики їх роботи відрізняються від колекцій, що змінюються. Якщо порівнювати з блокувальною колекцією, що змінюється, результати залежатимуть від вмісту блокування та зразків доступу. Однак взято з іншої публікації в блозі про незмінні колекції:

З: Я чув, що незмінні колекції повільні. Чи такі різні? Чи можу я їх використовувати, коли важлива продуктивність або пам'ять?

Відповідь: Ці незмінні колекції були налаштовані на конкурентоспроможні характеристики продуктивності колекцій, що змінюються, при цьому балансуючи обмін пам'яттю. В деяких випадках вони дуже швидко такі, як колекції мутацій як алгоритмічно, так і в реальному часі, іноді навіть швидше, в інших випадках вони алгоритмічно складніші. Однак у багатьох випадках різниця буде незначною. Як правило, вам слід скористатися найпростішим кодом, щоб виконати роботу, а потім налаштуватися на ефективність, якщо це необхідно. Незмінні колекції допомагають писати простий код, особливо коли потрібно враховувати безпеку ниток.

Іншими словами, в багатьох випадках різниця не буде помітна, і вам слід скористатися більш простим вибором - який для одночасних наборів було б використовувати ImmutableHashSet<T>, оскільки у вас немає існуючої реалізації блокуючої змінної! :-)


1
ImmutableHashSet<T>не дуже допомагає, якщо ваша мета - оновити загальний стан із кількох потоків або я щось тут пропускаю?
— tugberk

7
@tugberk Так і ні. Оскільки набір незмінний, вам доведеться оновити посилання на нього, що сама колекція вам не допомагає. Хороша новина полягає в тому, що ви зменшили складну проблему оновлення спільної структури даних з декількох потоків до набагато простішої проблеми оновлення загальної посилання. Бібліотека надає вам метод ImmutableInterlocked.Update, який допоможе вам у цьому.
— Søren Boisen

1
@ SørenBoisenjust читав про незмінні колекції і намагався розібратися, як їх використовувати безпечно. ImmutableInterlocked.UpdateЗдається, це відсутнє посилання. Дякую!
— xneg

4

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

Ви втрачаєте переваги, ConcurrentDictionary<T,byte>коли вам доведеться заблокувати весь набір під час ітерації. Без блокування ці операції не є безпечними для потоків.

Враховуючи додаткові накладні витрати ConcurrentDictionary<T,byte>, можливо, розумніше просто використовувати легшу вагу HashSet<T>і просто оточити все замками.

Якщо вам не потрібні задані операції, використовуйте ConcurrentDictionary<T,byte>та просто використовуйте default(byte)як значення під час додавання ключів.


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 та його змінену подію). Я відкритий для пропозицій, якщо ви можете додати знання до мого розуміння цього питання. Я витратив пару днів на пошуки сміття і мені завжди цікаво нова інформація
— Дбр

@ AndreasMüller хороша відповідь, проте мені цікаво, чому ви використовуєте '_lock.EnterWriteLock ();', а потім '_lock.EnterReadLock ();' у деяких методах, таких як "IntersectWith", я думаю, що тут немає необхідності шукати прочитане, оскільки блокування запису запобігає будь-якому читанню, якщо вводиться за замовчуванням.
— Джалал Саїд

Якщо завжди потрібно EnterWriteLock, чому це EnterReadLockвзагалі існує? Чи не може блокування читання використовуватись для таких методів Contains?
— ErikE

2
Це не ConcurrentHashSet, а скоріше ThreadSafeHashSet. Дивіться мій коментар до відповіді @ZenLulz щодо самореалізації. Я на 99% впевнений, що кожен, хто використовував ці реалізації, матиме серйозну помилку в їх застосуванні.
— Джордж Маврицакіс,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.