Паралельний 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.