Немає ConcurrentList <T> у .Net 4.0?


198

Я був в захваті від того, що побачив нову область System.Collections.Concurrentімен у. Net 4.0, досить приємно! Я бачив ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBagі BlockingCollection.

Одне, чого, здається, загадково не вистачає, - це ConcurrentList<T>. Чи повинен я сам це написати (або зняти з Інтернету :))?

Я пропускаю тут щось очевидне?



4
@RodrigoReis, ConcurrentBag <T> - це не упорядкована колекція, тоді як Список <T> впорядкований.
Адам Калвет Бол

4
Як ви могли мати замовлену колекцію у багатопотоковому середовищі? Ви ніколи не мали б керувати послідовністю елементів за дизайном.
Джеремі Головач

Використовуйте замість цього блокування
Ерік Бергштедт

У вихідному коді dotnet є файл під назвою ThreadSafeList.cs, який виглядає багато, як якийсь код нижче. Він також використовує ReaderWriterLockSlim і намагався з'ясувати, навіщо використовувати це замість простого блокування (obj)?
colin lamarre

Відповіді:


166

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

По-перше, немає жодного способу отримати повну реалізацію IList<T>цього безблочного та безпечного для потоків. Зокрема, випадкові вставки та видалення не спрацюють, якщо ви також не забудете про O (1) випадковий доступ (тобто, якщо ви не "обманюєте" і просто не використовуєте якийсь пов'язаний список і не дозволяєте індексувати смоктати).

Мені здалося, що це варто, це обмежена підмножина, захищена від потоків IList<T>: зокрема, така, яка дозволить Addі надає випадковий доступ лише для читання за допомогою індексу (але ні Insert, RemoveAtі т. Д., А також не має доступу до випадкового запису ).

Це і було метою моєї ConcurrentList<T>реалізації . Але коли я перевірив його ефективність у багатопотокових сценаріях, я виявив, що просто синхронізація додає до A List<T>швидше . В основному, додавання до a List<T>вже блискавично; складність обчислювальних кроків, що займаються, є незначною (збільшити індекс і призначити елементу в масиві; це дійсно все ). Вам знадобиться багато тонн одночасних записів, щоб побачити будь-яку суперечку про блокування щодо цього; і навіть тоді середня продуктивність кожного запису все-таки виграла б більш дорогу, хоча і безмежну реалізацію в ConcurrentList<T>.

У відносно рідкісних випадках, коли внутрішній масив списку повинен змінити розмір, ви платите невеликі витрати. Тому в кінцевому підсумку я прийшов до висновку, що це єдиний сценарій, коли ConcurrentList<T>тип колекції з додатковою базою мав би сенс: коли потрібно гарантувати низькі накладні витрати на додавання елемента на кожен виклик (так, на відміну від амортизованої мети виконання).

Це просто не настільки корисний клас, як ви могли подумати.


52
І якщо вам потрібне щось подібне до того, List<T>що використовує синхронізацію на основі монітора на старійSynchronizedCollection<T>
школі,

8
Одне невелике доповнення: використовуйте параметр конструктора Capacity, щоб уникнути (наскільки це можливо) сценарію зміни розміру.
Henk Holterman

2
Найбільшим сценарієм, де ConcurrentListвиграш був би, коли до списку не додається багато активності, але багато читачів одночасно. Можна було б зменшити накладні витрати читачів до єдиного бар'єру пам’яті (і усунути навіть те, якщо читачів не хвилювали б трохи застарілі дані).
supercat

2
@Kevin: Дуже тривіально побудувати ConcurrentList<T>так, що читачам гарантовано бачити стійкий стан, не потребуючи жодного блокування, із відносно невеликими надбавками. Коли список розшириться, наприклад, від розміру 32 до 64, збережіть масив size-32 та створіть новий масив розміру-64. Додаючи кожен з наступних 32 елементів, покладіть його в слот 32-63 нового масиву і скопіюйте старий елемент з масиву розміру-32 в новий. Поки не буде додано 64-й елемент, читачі будуть шукати масив розміром 32 для елементів 0-31 та масив розміру-64 для елементів 32-63.
supercat

2
Після додавання 64-го елемента масив розміру-32 все ще буде працювати для отримання елементів 0-31, але читачам більше не потрібно буде його використовувати. Вони можуть використовувати масив розмір-64 для всіх елементів 0-63, а масив розміру-128 для елементів 64-127. Накладні витрати на вибір одного з двох масивів та бар'єр пам’яті за бажанням будуть меншими, ніж накладні витрати навіть найефективнішого блокування читача-письменника. Написи, ймовірно, повинні використовувати блокування (без блокування це було б можливо, особливо якщо хтось не проти створити новий екземпляр об'єкта з кожною вставкою, але замок повинен бути дешевим.
supercat

38

Для чого б ви використовували ConcurrentList?

Концепція контейнера з випадковим доступом у потоковому світі не така корисна, як може здатися. Заява

  if (i < MyConcurrentList.Count)  
      x = MyConcurrentList[i]; 

в цілому все одно не буде безпечним для ниток.

Замість того, щоб створювати ConcurrentList, спробуйте створити рішення з того, що там є. Найпоширеніші класи - це ConcurrentBag і особливо BlockingCollection.


Гарна думка. І все-таки те, що я роблю, є трохи більш мирським. Я просто намагаюся призначити ConcurrentBag <T> в IList <T>. Я міг би переключити своє майно на IEnumerable <T>, але тоді я не можу .Додати до нього речі.
Алан

1
@Alan: Немає способів реалізувати це без блокування списку. Оскільки ви вже можете використовувати це Monitorдля будь-якого випадку, немає жодної причини для паралельного переліку.
Біллі ONeal

6
@dcp - так це по суті не є безпечним для потоків. ConcurrentDictionary має спеціальні методи, які роблять це в одній атомній операції, наприклад, AddOrUpdate, GetOrAdd, TryUpdate тощо. Вони все ще містять ContainsKey, тому що іноді просто потрібно знати, чи є ключ там, не змінюючи словник (думаю, HashSet)
Zarat

3
@dcp - ContainsKey сам по собі є безпечним для потоків, ваш приклад (не ContainsKey!) просто має перегоновий стан, оскільки ви робите другий дзвінок залежно від першого рішення, яке може бути вже застарілим.
Зарат

2
Хенк, я не згоден. Я думаю, що існує простий сценарій, коли це може бути дуже корисним. Запис робочої нитки в неї дозволить інтерфейсу читання та оновлення інтерфейсу користувача відповідно. Якщо ви хочете додати елемент сортованим способом, він вимагатиме запису у випадковому доступі. Ви також можете використовувати стек і перегляд даних, але вам доведеться підтримувати 2 колекції :-(.
Eric Ouellet

19

При всій належній повазі до чудових відповідей, вже наданих, я просто хочу отримати безпечний IList. Нічого просунутого або фантазійного. Продуктивність важлива у багатьох випадках, але часом це не викликає особливих проблем. Так, завжди існуватимуть проблеми без таких методів, як "TryGetValue" тощо, але в більшості випадків я просто хочу щось, що можу перерахувати, не турбуючись про те, щоб поставити замки навколо всього. І так, хтось, ймовірно, може знайти якийсь "помилку" в моїй реалізації, який може призвести до тупикової ситуації або чогось іншого (я вважаю), але нехай буде чесним: якщо мова йде про багатопотоковість, якщо ви не пишете свій код правильно, це все одно йде в глухий кут. Зважаючи на це, я вирішив зробити просту реалізацію ConcurrentList, яка забезпечує ці основні потреби.

І для чого це варто: я зробив основний тест, додавши 10 000 000 предметів до звичайних List і ConcurrentList, і результати були:

Список закінчений: 7793 мілісекунд. Закінчений час: 8064 мілісекунди.

public class ConcurrentList<T> : IList<T>, IDisposable
{
    #region Fields
    private readonly List<T> _list;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructors
    public ConcurrentList()
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>();
    }

    public ConcurrentList(int capacity)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(capacity);
    }

    public ConcurrentList(IEnumerable<T> items)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(items);
    }
    #endregion

    #region Methods
    public void Add(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Add(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void Insert(int index, T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Insert(index, item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            return this._list.Remove(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void RemoveAt(int index)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.RemoveAt(index);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public int IndexOf(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.IndexOf(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void Clear()
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Clear();
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.Contains(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        try
        {
            this._lock.EnterReadLock();
            this._list.CopyTo(array, arrayIndex);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    ~ConcurrentList()
    {
        this.Dispose(false);
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
            GC.SuppressFinalize(this);

        this._lock.Dispose();
    }
    #endregion

    #region Properties
    public T this[int index]
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list[index];
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
        set
        {
            try
            {
                this._lock.EnterWriteLock();
                this._list[index] = value;
            }
            finally
            {
                this._lock.ExitWriteLock();
            }
        }
    }

    public int Count
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list.Count;
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
    #endregion
}

    public class ConcurrentEnumerator<T> : IEnumerator<T>
{
    #region Fields
    private readonly IEnumerator<T> _inner;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructor
    public ConcurrentEnumerator(IEnumerable<T> inner, ReaderWriterLockSlim @lock)
    {
        this._lock = @lock;
        this._lock.EnterReadLock();
        this._inner = inner.GetEnumerator();
    }
    #endregion

    #region Methods
    public bool MoveNext()
    {
        return _inner.MoveNext();
    }

    public void Reset()
    {
        _inner.Reset();
    }

    public void Dispose()
    {
        this._lock.ExitReadLock();
    }
    #endregion

    #region Properties
    public T Current
    {
        get { return _inner.Current; }
    }

    object IEnumerator.Current
    {
        get { return _inner.Current; }
    }
    #endregion
}

5
Добре, стара відповідь, але все-таки: RemoveAt(int index)ніколи не Insert(int index, T item)є безпечною для потоків, безпечною є лише для індексу == 0, повернення IndexOf()одразу застаріло тощо. Навіть не починати з this[int].
Хенк Холтерман

2
І вам не потрібен і не хочеться ~ Finalizer ().
Хенк Холтерман

2
Ви говорите , що ви відмовилися від запобігання можливості тупика - і один ReaderWriterLockSlimможе бути тупикової ситуації легко , використовуючи EnterUpgradeableReadLock()одночасно. Однак ви не використовуєте його, ви не робите блокування доступним зовні, і ви, наприклад, не називаєте метод, який вводить блокування запису, утримуючи блокування читання, тому використання вашого класу більше не робить тупиків ймовірно.
Євген Бересовський

1
Не одночасний інтерфейс не підходить для одночасного доступу. Наприклад, наступне не є атомним var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";. Взагалі, будь-який комбінований читання-запис може привести вас до глибоких неприємностей, якщо робити це одночасно. Тому паралельні структури даних зазвичай надають методи для тих, як ConcurrentDictionary«s і AddOrGetт.д. NB Ваша постійна (і зайвими , оскільки члени вже позначені як такі підкреслення) повторення this.Клаттерів.
Євген Бересовський

1
Дякую Євгену. Я важкий користувач .NET Reflector, який ставить "це". на всіх нестатичних полях. Як такий, я виріс віддавати перевагу тому ж. Оскільки цей неконкурентний інтерфейс не є підходящим: ви абсолютно праві, що спроба виконати кілька дій проти моєї реалізації може стати ненадійною. Але вимога тут полягає лише в тому, що окремі дії (додавання, видалення, очищення чи перерахування) можна виконати без пошкодження колекції. Це в основному усуває необхідність ставити заяви про блокування навколо всього.
Брайан Бут

11

ConcurrentList(як масив зміни розміру, а не зв'язаний список) не легко написати з операціями, що не блокують. Його API не добре перекладається на "одночасну" версію.


12
Писати не тільки важко, навіть складно розібратися в корисному інтерфейсі.
CodesInChaos

11

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

int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");

Ефект, до якого йде автор, - це вставити "собаку" перед "кішкою", але в багатопотоковому середовищі зі списком між цими двома рядками коду може статися все, що завгодно. Наприклад, може зробити інший потік list.RemoveAt(0), перемістивши весь список вліво, але в принципі , catIndex не зміниться. Вплив тут полягає в тому, що Insertоперація насправді поставить «собаку» за котом, а не перед цим.

Кілька реалізацій, які ви бачите як запропоновані як "відповіді" на це запитання, є цілеспрямованими, але, як показано вище, вони не дають надійних результатів. Якщо ви дійсно хочете, щоб семантика, схожа на список, у багатопотоковому середовищі, ви не можете потрапити туди, поставивши блоки всередині методів реалізації списку. Ви повинні переконатися, що будь-який індекс, який ви використовуєте, живе цілком всередині контексту блокування. Підсумок полягає в тому, що ви можете використовувати Список у багатопотоковому середовищі з правильним блокуванням, але сам список не може бути таким, щоб існувати у цьому світі.

Якщо ви думаєте, що вам потрібен паралельний список, є дійсно дві можливості:

  1. Те, що вам справді потрібно - це ConcurrentBag
  2. Вам потрібно створити власну колекцію, можливо, реалізовану зі списком та власним контролем одночасності.

Якщо у вас є ConcurrentBag і ви перебуваєте в положенні, коли вам потрібно передати його як IList, у вас виникає проблема, оскільки метод, який ви викликаєте, вказав, що вони можуть спробувати зробити щось подібне до мене з котом & собака. У більшості світів це означає, що метод, який ви викликаєте, просто не побудований для роботи в багатопотоковому середовищі. Це означає, що ви або рефакторируйте його таким чином, щоб він був, або, якщо ви не можете, вам доведеться поводитися з цим дуже обережно. Від вас майже напевно буде потрібно створити власну колекцію із власними замками та зателефонувати до способу, що порушує її.


5

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

Показана нижче реалізація є

  • без замків
  • надзвичайно швидко для одночасного зчитування , навіть якщо одночасні модифікації тривають - незалежно від того, скільки часу вони займуть
  • оскільки "знімки" незмінні, можлива безблочна атомність , тобто var snap = _list; snap[snap.Count - 1];ніколи не буде (ну, крім порожнього списку, звичайно), і ви також отримаєте безпечне поточне перерахування з семантикою знімків безкоштовно .. як я люблю незмінність!
  • реалізовано загалом , застосовно до будь-якої структури даних та будь-якого типу модифікацій
  • мертвий простий , тобто легко перевірити, налагодити, перевірити, прочитавши код
  • може використовуватись у .Net 3.5

Щоб робота з копією на запис працювала, ви повинні зберігати структуру даних ефективно непорушною , тобто нікому не дозволяється змінювати їх після того, як ви зробили їх доступними для інших потоків. Коли ви хочете змінити, ви

  1. клонувати структуру
  2. внести модифікації на клон
  3. атомарний своп у посиланні на модифікований клон

Код

static class CopyOnWriteSwapper
{
    public static void Swap<T>(ref T obj, Func<T, T> cloner, Action<T> op)
        where T : class
    {
        while (true)
        {
            var objBefore = Volatile.Read(ref obj);
            var newObj = cloner(objBefore);
            op(newObj);
            if (Interlocked.CompareExchange(ref obj, newObj, objBefore) == objBefore)
                return;
        }
    }
}

Використання

CopyOnWriteSwapper.Swap(ref _myList,
    orig => new List<string>(orig),
    clone => clone.Add("asdf"));

Якщо вам потрібна більша продуктивність, це допоможе знищити метод, наприклад, створити один метод для кожного типу модифікацій (Додати, видалити, ...), який ви хочете, і жорсткий код покажчиків функцій clonerта op.

Примітка №1. Ви несете відповідальність за те, щоб ніхто не змінював структуру даних (нібито). Ми нічого не можемо зробити в загальному реалізації, щоб цього не допустити, але, спеціалізуючись на List<T>, ви можете захиститись від модифікації за допомогою List.AsReadOnly ()

NB №2 Будьте уважні до значень у списку. Підхід під час копіювання над написанням захищає лише їхнє членство в списку, але якщо ви розміщуєте там не рядки, а деякі інші змінні об'єкти, вам слід подбати про безпеку потоку (наприклад, блокування). Але це є ортогональним для цього рішення, і наприклад, фіксація змінних значень може бути легко використана без проблем. Вам це просто потрібно знати.

Примітка №3. Якщо ваша структура даних є величезною, і ви часто її змінюєте, підхід під час копіювання на запис може бути забороняючим як щодо споживання пам’яті, так і вартості процесора для копіювання. У цьому випадку ви можете замість цього використати MS Immutable Collections .


3

System.Collections.Generic.List<t>вже безпечно для кількох читачів. Намагатися зробити його потоком безпечним для декількох авторів не має сенсу. (З причин, про які вже згадували Хенк і Стівен)


Ви не можете бачити сценарій, коли я можу додати до списку 5 потоків? Таким чином, ви могли бачити, як список накопичує записи ще до того, як всі вони припиняться.
Алан

9
@Alan - це би ConcurrentQueue, ConcurrentStack або ConcurrentBag. Щоб мати сенс ConcurrentList, вам слід надати випадок використання, коли наявних класів недостатньо. Я не бачу, чому я хотів би індексувати доступ, коли елементи в індексах можуть випадковим чином змінюватися через одночасне видалення. А для "заблокованого" читання ви вже можете робити знімки існуючих одночасних класів і поміщати їх у список.
Зарат

Ви маєте рацію - я не хочу індексованого доступу. Я, як правило, використовую IList <T> як проксі для IE Число, до якого я можу .Додати (T) нові елементи. От звідси випливає питання.
Алан

@Alan: Тоді ви хочете чергу, а не список.
Біллі ONeal

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

2

Деякі люди приховували деякі пункти товарів (і деякі мої думки):

  • Це може виглядати божевільним до невдалого випадкового доступу (індексатора), але мені це здається прекрасним. Потрібно лише думати, що існує багато методів у багатопотокових колекціях, які можуть вийти з ладу, як Indexer та Delete. Ви також можете визначити відмову (резервну) дію для аксесуара для запису на кшталт "невдача" або просто "додати в кінці".
  • Це не тому, що це багатопотокова колекція, яка завжди буде використовуватися в багатопотоковому контексті. Або ним також міг би користуватися лише один письменник і один читач.
  • Іншим способом безпечного використання індексатора може бути загортання дій у замок колекції за допомогою його кореня (якщо він оприлюднений).
  • Для багатьох людей зробити так, щоб вигляд rootLock був видимим - це агаїст "Гарна практика". Я не на 100% впевнений у цьому питанні, тому що якщо він прихований, ви видалите користувачеві велику гнучкість. Ми завжди повинні пам’ятати, що програмування багатопотокових записів не для кого-небудь. Ми не можемо запобігти неправильному використанню.
  • Microsoft доведеться виконати певну роботу і визначити новий стандарт, щоб запровадити правильне використання багатопотокової колекції. По-перше, IEnumerator не повинен мати moveNext, але повинен мати GetNext, який повертає true або false і отримує параметр типу T (таким чином ітерація більше не блокується). Крім того, Microsoft вже використовує "використання" внутрішньо в foreach, але іноді використовує IEnumerator безпосередньо, не загортаючи його з "using" (помилка у вікні колекції і, ймовірно, в інших місцях). Ця помилка видаляє хороший потенціал для безпечного ітератора ... Ітератор, який блокує збір у конструкторі та розблокує його метод Dispose - для блокування методу foreach.

Це не відповідь. Це лише коментарі, які насправді не підходять до конкретного місця.

... Мій висновок, Microsoft має внести деякі глибокі зміни в "передчуття", щоб зробити колекцію MultiThreaded простішою у використанні. Крім того, він повинен дотримуватись власних правил використання IEnumerator. До цього ми можемо легко написати MultiThreadList, який би використовував блокуючий ітератор, але це не буде слідувати "IList". Натомість вам доведеться визначити власний інтерфейс "IListPersonnal", який може вийти з ладу на "вставити", "вилучити" та випадковий доступ (індекс) без винятку. Але хто захоче його використовувати, якщо він не є стандартним?


Можна було б легко написати текст, ConcurrentOrderedBag<T>який би включав в себе реалізацію лише для читання IList<T>, але також запропонував би безпечний int Add(T value)метод, що забезпечує потік . Я не бачу, чому ForEachпотрібні якісь зміни. Хоча Microsoft прямо не говорить про це, їх практика показує, що цілком прийнятно IEnumerator<T>перераховувати вміст колекції, який існував при його створенні; виняток, модифікований колекцією, потрібен лише в тому випадку, якщо перечислювач не зможе гарантувати роботу без збоїв.
supercat

Ітерація через колекцію MT, її дизайн може призвести, як ви сказали, до винятку ... Якого я не знаю. Чи хотіли б ви захопити всі винятки? У моїй власній книзі виняток є винятком і не має відбуватися при нормальному виконанні коду. В іншому випадку, щоб запобігти винятку, вам доведеться або заблокувати колекцію, або отримати копію (безпечним способом, тобто блокуванням), або застосувати в колекції дуже складний механізм, щоб запобігти виникненню винятку через одночасність. Моє те було те, що було б непогано додати IEnumeratorMT, який би заблокував колекцію, а для кожного відбувся і додав відповідний код ...
Eric Ouellet

Інша річ, яка також може статися, це те, що коли ви отримуєте ітератор, ви можете заблокувати колекцію, а коли ваш ітератор зібраний GC, ви можете розблокувати колекцію. За словами Microsfot, вони вже перевіряють, чи є IEnumerable також ідентифікатором, і викликають GC, якщо так, в кінці ForEach. Основна проблема полягає в тому, що вони також використовують IEnumerable в іншому місці, не викликаючи GC, ви не можете на це покластися. Наявність нового чіткого інтерфейсу MT для IEnumerable увімкнення блокування вирішить проблему, принаймні, частину її. (Це не завадило б людям не називати це).
Ерік Оуеллет

Дуже погана форма для публічного GetEnumeratorметоду залишати колекцію заблокованою після її повернення; такі конструкції можуть легко призвести до тупику. IEnumerable<T>Не дає ніяких вказівок про те , перерахування можна очікувати завершення , навіть якщо колекція модифікується; найкраще, що можна зробити, - це написати власні методи, щоб вони це зробили, і мати методи, які приймають IEnumerable<T>документ, той факт, який буде безпечним для потоків, лише якщо IEnumerable<T>підтримує безпечне потокове перерахування.
supercat

Що було б найбільш корисно було б, якби IEnumerable<T>було включено метод "Знімок" з типом повернення IEnumerable<T>. Незмінні колекції можуть повернутися самі; обмежена колекція може, якщо нічого іншого не скопіювати себе на List<T>або T[]зателефонувати GetEnumeratorна це. Деякі безмежні колекції можуть бути реалізовані Snapshot, а ті, які не змогли б викинути виняток, не намагаючись заповнити список їхнім вмістом.
supercat

1

У послідовному виконанні коду використовувані структури даних відрізняються від (добре написаного) паралельно виконуваного коду. Причина полягає в тому, що послідовний код передбачає неявний порядок. Паралельний код, однак, не передбачає жодного замовлення; ще краще, це передбачає відсутність будь-якого визначеного порядку!

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

Пам'ятайте, що одночасність - це проблема даних, а не проблема коду! Ви не можете спершу реалізувати код (або переписати існуючий послідовний код) і отримати добре розроблене одночасне рішення. Спочатку потрібно розробити структури даних, маючи на увазі, що неявне впорядкування не існує в паралельній системі.


1

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

public class CopyAndWriteList<T>
{
    public static List<T> Clear(List<T> list)
    {
        var a = new List<T>(list);
        a.Clear();
        return a;
    }

    public static List<T> Add(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Add(item);
        return a;
    }

    public static List<T> RemoveAt(List<T> list, int index)
    {
        var a = new List<T>(list);
        a.RemoveAt(index);
        return a;
    }

    public static List<T> Remove(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Remove(item);
        return a;
    }

}

приклад використання: orders_BUY = CopyAndWriteList.Clear (замовлення_BUY);


замість блокування він створює копію списку, змінює список і встановлює посилання на новий список. Тож будь-які інші потоки, які повторюються, не спричинять проблем.
Роб Кількість

0

Я реалізував один подібний до Брайана . Моя різна:

  • Я керую масивом безпосередньо.
  • Я не ввожу блокування в блоці спробу.
  • Я використовую yield returnдля виготовлення нумератора.
  • Я підтримую рекурсію блокування. Це дозволяє читати зі списку під час ітерації.
  • Я використовую оновлені блокування читання, де це можливо.
  • DoSyncта GetSyncметоди, що дозволяють послідовно взаємодіяти, що вимагає ексклюзивного доступу до списку.

Код :

public class ConcurrentList<T> : IList<T>, IDisposable
{
    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    private int _count = 0;

    public int Count
    {
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _count;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    public int InternalArrayLength
    { 
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _arr.Length;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    private T[] _arr;

    public ConcurrentList(int initialCapacity)
    {
        _arr = new T[initialCapacity];
    }

    public ConcurrentList():this(4)
    { }

    public ConcurrentList(IEnumerable<T> items)
    {
        _arr = items.ToArray();
        _count = _arr.Length;
    }

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {       
            var newCount = _count + 1;          
            EnsureCapacity(newCount);           
            _arr[_count] = item;
            _count = newCount;                  
        }
        finally
        {
            _lock.ExitWriteLock();
        }       
    }

    public void AddRange(IEnumerable<T> items)
    {
        if (items == null)
            throw new ArgumentNullException("items");

        _lock.EnterWriteLock();

        try
        {           
            var arr = items as T[] ?? items.ToArray();          
            var newCount = _count + arr.Length;
            EnsureCapacity(newCount);           
            Array.Copy(arr, 0, _arr, _count, arr.Length);       
            _count = newCount;
        }
        finally
        {
            _lock.ExitWriteLock();          
        }
    }

    private void EnsureCapacity(int capacity)
    {   
        if (_arr.Length >= capacity)
            return;

        int doubled;
        checked
        {
            try
            {           
                doubled = _arr.Length * 2;
            }
            catch (OverflowException)
            {
                doubled = int.MaxValue;
            }
        }

        var newLength = Math.Max(doubled, capacity);            
        Array.Resize(ref _arr, newLength);
    }

    public bool Remove(T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {           
            var i = IndexOfInternal(item);

            if (i == -1)
                return false;

            _lock.EnterWriteLock();
            try
            {   
                RemoveAtInternal(i);
                return true;
            }
            finally
            {               
                _lock.ExitWriteLock();
            }
        }
        finally
        {           
            _lock.ExitUpgradeableReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        _lock.EnterReadLock();

        try
        {    
            for (int i = 0; i < _count; i++)
                // deadlocking potential mitigated by lock recursion enforcement
                yield return _arr[i]; 
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private int IndexOfInternal(T item)
    {
        return Array.FindIndex(_arr, 0, _count, x => x.Equals(item));
    }

    public void Insert(int index, T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {                       
            if (index > _count)
                throw new ArgumentOutOfRangeException("index"); 

            _lock.EnterWriteLock();
            try
            {       
                var newCount = _count + 1;
                EnsureCapacity(newCount);

                // shift everything right by one, starting at index
                Array.Copy(_arr, index, _arr, index + 1, _count - index);

                // insert
                _arr[index] = item;     
                _count = newCount;
            }
            finally
            {           
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }


    }

    public void RemoveAt(int index)
    {   
        _lock.EnterUpgradeableReadLock();
        try
        {   
            if (index >= _count)
                throw new ArgumentOutOfRangeException("index");

            _lock.EnterWriteLock();
            try
            {           
                RemoveAtInternal(index);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }
    }

    private void RemoveAtInternal(int index)
    {           
        Array.Copy(_arr, index + 1, _arr, index, _count - index-1);
        _count--;

        // release last element
        Array.Clear(_arr, _count, 1);
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {        
            Array.Clear(_arr, 0, _count);
            _count = 0;
        }
        finally
        {           
            _lock.ExitWriteLock();
        }   
    }

    public bool Contains(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item) != -1;
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {       
        _lock.EnterReadLock();
        try
        {           
            if(_count > array.Length - arrayIndex)
                throw new ArgumentException("Destination array was not long enough.");

            Array.Copy(_arr, 0, array, arrayIndex, _count);
        }
        finally
        {
            _lock.ExitReadLock();           
        }
    }

    public bool IsReadOnly
    {   
        get { return false; }
    }

    public T this[int index]
    {
        get
        {
            _lock.EnterReadLock();
            try
            {           
                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                return _arr[index]; 
            }
            finally
            {
                _lock.ExitReadLock();               
            }           
        }
        set
        {
            _lock.EnterUpgradeableReadLock();
            try
            {

                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                _lock.EnterWriteLock();
                try
                {                       
                    _arr[index] = value;
                }
                finally
                {
                    _lock.ExitWriteLock();              
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }

        }
    }

    public void DoSync(Action<ConcurrentList<T>> action)
    {
        GetSync(l =>
        {
            action(l);
            return 0;
        });
    }

    public TResult GetSync<TResult>(Func<ConcurrentList<T>,TResult> func)
    {
        _lock.EnterWriteLock();
        try
        {           
            return func(this);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {   
        _lock.Dispose();
    }
}

Що станеться, якщо два потоки потрапляють одночасно на початок tryблоку Removeабо в індекс індексатора?
Джеймс

@James, що не здається можливим. Читайте зауваження в msdn.microsoft.com/en-us/library / ... . Запускаючи цей код, ви ніколи не входите в цей замок вдруге: gist.github.com/ronnieoverby/59b715c3676127a113c3
Ронні Овербі

@Ronny Overby: Цікаво. Зважаючи на це, я підозрюю, що це буде набагато краще, якщо ви видалите UpgradebleReadLock з усіх функцій, де єдина операція, виконана за час між оновленим блоком читання і блокуванням запису - накладні витрати на зняття будь-якого типу блокування набагато більше ніж перевірка, щоб переконатися, що параметр знаходиться поза діапазоном, що тільки що робить цю перевірку всередині блокування запису, швидше за все, буде краще.
Джеймс

Цей клас також не здається дуже корисним, оскільки функції, що базуються на зміщенні (більшість з них) дійсно ніколи не можуть бути безпечно використані, якщо все-таки не існує якоїсь зовнішньої схеми блокування, оскільки колекція може змінюватися між тим, коли ви вирішите, куди поставити або дістати щось від і коли ви насправді отримаєте це.
Джеймс

1
Мені хотілося продовжити запис, сказавши, що я визнаю, що корисність IListсемантики в паралельних сценаріях обмежена в кращому випадку. Я написав цей код, ймовірно, ще до того, як я прийшов до цього усвідомлення. Мій досвід такий же, як і письменник прийнятої відповіді: я спробував це з тим, що я знав про синхронізацію та IList <T>, і я щось дізнався, роблячи це.
Ронні Овербі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.