Нет ConcurrentList <T> в .Net 4.0?

198

Я был рад увидеть новое System.Collections.Concurrentпространство имен в .Net 4.0, довольно приятно! Я видел ConcurrentDictionary, ConcurrentQueue,ConcurrentStack , ConcurrentBagи BlockingCollection.

Одна вещь, которая, кажется, таинственно отсутствует, это ConcurrentList<T> . Я должен написать это сам (или убрать это из сети :))?

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

Алан
источник
7
ConcurrentBag <T> ( msdn.microsoft.com/pt-br/library/… )
Родриго Рейс
4
@RodrigoReis, ConcurrentBag <T> - неупорядоченная коллекция, а List <T> упорядочен.
Адам Кальвет Бол
4
Как вы могли иметь упорядоченную коллекцию в многопоточной среде? Вы никогда не будете иметь контроль над последовательностью элементов по дизайну.
Джереми Холовач
Вместо этого используйте замок
Эрик
в исходном коде dotnet есть файл ThreadSafeList.cs, который выглядит примерно так, как показано ниже. Он также использует ReaderWriterLockSlim и пытался выяснить, зачем использовать это вместо простой блокировки (obj)?
Колин Ламарр

Ответы:

166

Я дал ему попытку некоторое время назад (также: на GitHub ). В моей реализации были некоторые проблемы, о которых я не буду здесь говорить. Позвольте мне рассказать вам, что более важно, что я узнал.

Во-первых, вы не сможете получить полную реализацию без IList<T>блокировки и поточно-ориентированной обработки. В частности, случайные вставки и удаления не будут работать, если вы также не забудете о O (1) произвольном доступе (то есть, если вы не «обманываете» и просто используете какой-то связанный список и не позволяете индексированию сосать).

То , что я думал , что может быть полезным было потокобезопасного, ограниченное подмножество IList<T>: в частности, один , что позволило бы Addи не обеспечивают случайного чтения только доступ по индексу (но не Insert, RemoveAtи т.д., а также не случайная запись доступа).

Это было целью моей ConcurrentList<T>реализации . Но когда я проверил его производительность в многопоточных сценариях, я обнаружил, что простая синхронизация добавления к a List<T>была быстрее . По сути, добавление к уже List<T>молниеносно; сложность используемых вычислительных шагов очень мала (увеличить индекс и присвоить элементу в массиве; это действительно так ). Вам потребуется тонна одновременных записей, чтобы увидеть какие-либо конфликты блокировок по этому вопросу; и даже тогда средняя производительность каждой записи все равно побьет более дорогую, хотя и без блокировки, реализацию вConcurrentList<T> .

В относительно редком случае, когда внутренний массив списка должен изменить свой размер, вы платите небольшую цену. В итоге я пришел к выводу, что это был единственный нишевой сценарий, в котором имел ConcurrentList<T>бы смысл тип коллекции « только для добавления» : когда вы хотите гарантированно низкие накладные расходы на добавление элемента при каждом вызове (так, в отличие от амортизированной цели производительности).

Это просто не такой полезный класс, как вы думаете.

Дэн Тао
источник
52
И если вам нужно что-то подобное тому, List<T>что использует old-skool, синхронизацию на основе монитора, SynchronizedCollection<T>в BCL есть скрытое: msdn.microsoft.com/en-us/library/ms668265.aspx
LukeH
8
Небольшое дополнение: используйте параметр конструктора Capacity, чтобы избежать (насколько это возможно) сценария изменения размера.
Хенк Холтерман
2
Самый большой сценарий, при котором ConcurrentListпобеда будет, - это когда в список добавляется не так много активности, но есть много одновременных читателей. Можно уменьшить накладные расходы читателей на один барьер памяти (и даже устранить это, если читателей не волнуют слегка устаревшие данные).
суперкат
2
@Kevin: Довольно просто создать ConcurrentList<T>такой способ, чтобы читатели гарантированно видели согласованное состояние без какой-либо блокировки, с относительно небольшими дополнительными издержками. Когда список увеличится, например, с размера 32 до 64, сохраните массив размера 32 и создайте новый массив размера 64. При добавлении каждого из следующих 32 элементов поместите его в слот 32-63 нового массива и скопируйте старый элемент из массива размера 32 в новый. Пока 64-й элемент не будет добавлен, читатели будут искать в массиве размера 32 элементы 0-31 и в массиве размера 64 элементы 32-63.
суперкат
2
После добавления 64-го элемента массив size-32 будет по-прежнему работать для извлечения элементов 0–31, но читателям больше не нужно будет его использовать. Они могут использовать массив размера 64 для всех элементов 0-63 и массив размера 128 для элементов 64-127. Затраты на выбор того, какой из двух массивов использовать, плюс барьер памяти, если это необходимо, будут меньше, чем издержки даже самой эффективной блокировки чтения-записи. При записи, вероятно, следует использовать блокировки (без блокировок было бы возможно, особенно если бы никто не возражал против создания нового экземпляра объекта при каждой вставке, но блокировка должна быть дешевой.
Суперкат
38

Для чего бы вы использовали ConcurrentList?

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

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

в целом все равно не будет потокобезопасным.

Вместо того, чтобы создавать ConcurrentList, попробуйте создать решения с тем, что там есть. Наиболее распространенными классами являются ConcurrentBag и особенно BlockingCollection.

Хенк Холтерман
источник
Хорошая точка зрения. Тем не менее, то, что я делаю, немного более обыденно. Я просто пытаюсь назначить ConcurrentBag <T> в IList <T>. Я мог бы переключить свое свойство на IEnumerable <T>, но тогда я не смогу. Добавить что-то к нему.
Алан
1
@ Алан: Нет способа реализовать это без блокировки списка. Поскольку вы уже можете использовать Monitorэто, в любом случае, нет причин для одновременного списка.
Билли ОНил
6
@dcp - да, это по сути не потокобезопасно. В ConcurrentDictionary есть специальные методы, которые делают это в одной элементарной операции, например, AddOrUpdate, GetOrAdd, TryUpdate и т. Д. Они по-прежнему имеют ContainsKey, потому что иногда вам просто нужно узнать, есть ли ключ без изменения словаря (например, HashSet)
Зарат
3
@dcp - ContainsKey сам по себе потокобезопасен, ваш пример (не ContainsKey!) просто имеет состояние гонки, потому что вы делаете второй вызов в зависимости от первого решения, которое на тот момент уже может быть устаревшим.
Зарат
2
Хенк, я не согласен. Я думаю, что есть простой сценарий, где он может быть очень полезным. Рабочий поток, записывающий в него поток, будет соответствующим образом читать и обновлять интерфейс. Если вы хотите добавить элемент в отсортированном виде, это потребует произвольной записи. Вы также можете использовать стек и просмотр данных, но вам нужно будет поддерживать 2 коллекции :-(.
Эрик Оуэлл
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
И вам не нужен и не нужен финализатор ().
Хенк Холтерман
2
Вы говорите , что вы отказались от предотвращения возможности тупика - и один ReaderWriterLockSlimможет быть тупиковой ситуации легко , используя EnterUpgradeableReadLock()одновременно. Однако вы не используете его, вы не делаете блокировку доступной извне и, например, не вызываете метод, который вводит блокировку записи, удерживая блокировку чтения, поэтому использование вашего класса больше не приводит к блокировке скорее всего.
Евгений Бересовский
1
Одновременный интерфейс не подходит для одновременного доступа. Например, следующее не атомарно var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";. В общем, любая комбинация чтения-записи может привести к серьезным проблемам при одновременном выполнении. Вот почему параллельные структуры данных обычно предоставляют методы для таких, как ConcurrentDictionaryи AddOrGetт. Д. NB. Ваше постоянное (и избыточное, потому что элементы уже помечены как таковое подчеркиванием) повторение this.беспорядка.
Евгений Бересовский
1
Спасибо Евгений. Я большой пользователь .NET Reflector, который ставит «это». на всех нестатических полях. Таким образом, я вырос, чтобы предпочесть то же самое. Относительно несоответствующего интерфейса не подходит: вы абсолютно правы, что попытка выполнить несколько действий против моей реализации может стать ненадежной. Но здесь требуется просто, чтобы отдельные действия (добавление, удаление, очистка или перечисление) выполнялись без повреждения коллекции. Это в основном устраняет необходимость помещать операторы блокировки вокруг всего.
Брайан Бут
11

ConcurrentList(как массив с изменяемым размером, а не связанный список) нелегко написать с помощью неблокирующих операций. Его API плохо переводится в «параллельную» версию.

Стивен Клири
источник
12
Это не только сложно написать, но даже сложно понять полезный интерфейс.
CodesInChaos
11

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

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

Эффект, который преследует автор, заключается в том, чтобы вставить «dog» перед «cat», но в многопоточной среде между этими двумя строками кода может произойти что угодно. Например, другой поток может сделать это list.RemoveAt(0), сместив весь список влево, но, что важно, catIndex не изменится. Воздействие здесь является то , что Insertоперация будет на самом деле поставил «собаку» после того, как кошка, а не до него.

Несколько реализаций, которые вы видите в качестве «ответов» на этот вопрос, являются благонамеренными, но, как показано выше, они не дают надежных результатов. Если вы действительно хотите использовать семантику в виде списков в многопоточной среде, вы не сможете добиться этого, установив блокировки внутри методов реализации списков. Вы должны убедиться, что любой используемый вами индекс полностью живет в контексте блокировки. В результате вы можете использовать List в многопоточной среде с правильной блокировкой, но сам список не может быть создан в этом мире.

Если вы считаете, что вам нужен параллельный список, на самом деле есть только две возможности:

  1. Что вам действительно нужно, так это ConcurrentBag
  2. Вам необходимо создать свою собственную коллекцию, возможно, реализованную с помощью List и собственного управления параллелизмом.

Если у вас есть ConcurrentBag и вы находитесь в положении, когда вам нужно передать его как IList, у вас есть проблема, потому что метод, который вы вызываете, указал, что они могут попытаться сделать что-то, как я делал выше, с cat & собака. В большинстве миров это означает, что вызываемый вами метод просто не предназначен для работы в многопоточной среде. Это означает, что вы либо реорганизуете его так, чтобы оно было, либо, если вы не можете, вам придется обращаться с ним очень осторожно. Вы почти наверняка будете обязаны создать свою собственную коллекцию со своими собственными блокировками и вызвать вызывающий сбой метод внутри блокировки.

Стив Бенц
источник
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"));

Если вам нужна более высокая производительность, это поможет сгенерировать метод, например, создать один метод для каждого типа модификации (Add, Remove, ...), который вы хотите, и жестко кодировать указатели функций clonerи op.

NB # 1 Вы несете ответственность за то, чтобы никто не изменил (предположительно) неизменяемую структуру данных. Мы ничего не можем сделать в универсальной реализации, чтобы предотвратить это, но, специализируясь на этом List<T>, вы можете защититься от модификации, используя List.AsReadOnly ()

NB # 2 Будьте осторожны со значениями в списке. Приведенный выше подход копирования при записи защищает только их членство в списке, но если вы поместите туда не строки, а некоторые другие изменяемые объекты, вы должны позаботиться о безопасности потоков (например, блокировка). Но это ортогонально этому решению, и, например, блокировка изменяемых значений может быть легко использована без проблем. Вам просто нужно знать об этом.

NB # 3. Если ваша структура данных огромна, и вы часто ее модифицируете, подход «копировать все при записи» может быть чрезмерным как с точки зрения потребления памяти, так и с точки зрения затрат на копирование ЦП. В этом случае вы можете использовать неизменные коллекции MS .

Евгений Бересовский
источник
3

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

Билли ОНил
источник
Вы не видите сценарий, где я мог бы добавить 5 потоков в список? Таким образом, вы можете увидеть, как список накапливает записи еще до того, как все они завершатся.
Алан
9
@Alan - это будет ConcurrentQueue, ConcurrentStack или ConcurrentBag. Чтобы разобраться в ConcurrentList, вы должны предоставить пример использования, когда доступных классов недостаточно. Я не понимаю, зачем мне нужен индексированный доступ, когда элементы в индексах могут случайно меняться при одновременном удалении. А для «заблокированного» чтения вы уже можете сделать снимки существующих параллельных классов и поместить их в список.
Зарат
Вы правы - я не хочу индексированный доступ. Я обычно использую IList <T> в качестве прокси для IEnumerable, к которому я могу добавить .Add (T) новые элементы. Вот откуда вопрос, правда.
Алан
@ Алан: Тогда вы хотите очередь, а не список.
Билли ОНил
3
Я думаю, что вы не правы. Говоря: безопасно для нескольких читателей не означает, что вы не можете писать одновременно. Запись также будет означать удаление, и вы получите ошибку, если вы удалите ее во время итерации.
Эрик Оуэлл
2

Некоторые люди высветили некоторые пункты товара (и некоторые мои мысли):

  • Это может выглядеть безумным для неспособного случайного доступа (индексатора), но мне кажется, что это нормально. Нужно только думать, что в многопоточных коллекциях существует множество методов, которые могут завершиться с ошибкой, например, Indexer и Delete. Вы также можете определить действие сбоя (отступления) для средства доступа к записи, например «сбой» или просто «добавить в конце».
  • Не потому, что это многопоточная коллекция, она всегда будет использоваться в многопоточном контексте. Или это может также использоваться только одним писателем и одним читателем.
  • Другой способ безопасного использования индексатора может заключаться в том, чтобы обернуть действия в блокировку коллекции, используя ее корень (если он открыт).
  • Для многих людей сделать rootLock видимым - это хорошая практика. Я не уверен на 100% в этом вопросе, потому что, если он скрыт, вы теряете большую гибкость для пользователя. Мы всегда должны помнить, что программирование многопоточности не для кого-либо. Мы не можем предотвратить любое неправильное использование.
  • Microsoft придется проделать определенную работу и определить новый стандарт, чтобы ввести правильное использование многопоточной коллекции. Во-первых, IEnumerator не должен иметь moveNext, но должен иметь GetNext, который возвращает true или false и получает выходной параметр типа T (таким образом, итерация больше не будет блокировать). Кроме того, Microsoft уже использует «использование» внутри себя в foreach, но иногда использует IEnumerator напрямую, не заключая его в «использование» (ошибка в представлении коллекции и, возможно, в других местах). Использование IEnumerator в качестве обертки рекомендуется Microsoft. Эта ошибка удаляет хороший потенциал для безопасного итератора ... Итератор, который блокирует коллекцию в конструкторе и разблокирует его методом Dispose - для блокирующего метода foreach.

Это не ответ. Это только комментарии, которые не совсем соответствуют конкретному месту.

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

Эрик Оуэлл
источник
Можно легко написать, ConcurrentOrderedBag<T>который будет включать реализацию только для чтения IList<T>, но также будет предлагать полностью потокобезопасный int Add(T value)метод. Я не понимаю, зачем ForEachнужны какие-то изменения. Хотя Microsoft явно не говорит об этом, их практика показывает, что вполне допустимо IEnumerator<T>перечислять содержимое коллекции, существовавшее при ее создании; исключение, изменяющее коллекцию, требуется только в том случае, если перечислитель не сможет гарантировать работу без сбоев.
суперкат
Итерирование по коллекции MT, то, как она выглядит, может привести, как вы сказали, к исключению ... Которого я не знаю. Будете ли вы ловить все исключения? В моей собственной книге исключение является исключением и не должно происходить при нормальном выполнении кода. В противном случае, чтобы предотвратить исключение, вы должны либо заблокировать коллекцию, либо получить копию (безопасным способом, т. Е. Заблокировать), либо реализовать очень сложный механизм в коллекции, чтобы предотвратить возникновение исключения из-за параллелизма. Мое мнение состояло в том, что было бы неплохо добавить IEnumeratorMT, который будет блокировать коллекцию, в то время как для каждого происходит, и добавлять связанный код ...
Eric Ouellet
Другая вещь, которая также может произойти, это то, что когда вы получаете итератор, вы можете заблокировать коллекцию, а когда ваш итератор собран GC, вы можете разблокировать коллекцию. Согласно Microsfot они уже проверяют, является ли IEnumerable также IDisposable, и вызывают GC, если это так, в конце ForEach. Основная проблема заключается в том, что они также используют IEnumerable в другом месте без вызова GC, и вы не можете на это полагаться. Наличие нового понятного интерфейса MT для IEnumerable с включенной блокировкой решит проблему, по крайней мере, ее часть. (Это не помешало бы людям не называть это).
Эрик Оуэлл
Публичному GetEnumeratorметоду очень плохо оставлять коллекцию заблокированной после ее возвращения; такие конструкции могут легко привести к тупику. Он IEnumerable<T>не указывает, можно ли ожидать завершения перечисления, даже если коллекция изменена; лучшее, что можно сделать, - это написать собственные методы, чтобы они это делали, и иметь методы, которые принимают IEnumerable<T>документ, тот факт, что он будет поточно-ориентированным, только если IEnumerable<T>поддерживает поточно-перечислимое перечисление.
суперкат
То, что было бы наиболее полезным, было бы, если IEnumerable<T>бы включил метод «Снимок» с возвращаемым типом IEnumerable<T>. Неизменные коллекции могут вернуть себя; ограниченная коллекция может, если больше ничего не скопировать себя в List<T>или T[]и вызвать GetEnumeratorэто. Могут быть реализованы некоторые неограниченные коллекции Snapshot, а те, которые не смогут создать исключение, не пытаясь заполнить список своим содержимым.
суперкат
1

В последовательно выполняемом коде используемые структуры данных отличаются от (хорошо написанного) параллельно исполняемого кода. Причина в том, что последовательный код подразумевает неявный порядок. Параллельный код, однако, не подразумевает какого-либо порядка; еще лучше это означает отсутствие какого-либо определенного порядка!

Из-за этого структуры данных с подразумеваемым порядком (например, List) не очень полезны для решения параллельных задач. Список подразумевает порядок, но он не дает четкого определения, что это за порядок. Из-за этого порядок выполнения кода, управляющего списком, будет определять (до некоторой степени) неявный порядок списка, который находится в прямом конфликте с эффективным параллельным решением.

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

Blueprint41
источник
1

Подход без блокировок Copy and Write отлично работает, если вы не имеете дело с слишком большим количеством элементов. Вот класс, который я написал:

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 (orders_BUY);

Роб Квант
источник
вместо блокировки он создает копию списка, изменяет список и устанавливает ссылку на новый список. Таким образом, любые другие потоки, которые повторяются, не вызовут никаких проблем.
Роб Квант
0

Я реализовал один похожий на Брайана . Мой отличается:

  • Я управляю массивом напрямую.
  • Я не вхожу в замки в блоке try.
  • я использую 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или установщика индексатора одновременно?
Джеймс
@ Джеймс, который не представляется возможным. Прочтите замечания по адресу msdn.microsoft.com/en-us/library/… . Запустив этот код, вы никогда не будете вводить эту блокировку второй раз: gist.github.com/ronnieoverby/59b715c3676127a113c3
Ронни Оверби
@Ronny Overby: Интересно. Учитывая это, я подозреваю, что это будет работать намного лучше, если вы удалите UpgradableReadLock из всех функций, где единственная операция, выполняемая за время между обновляемой блокировкой чтения и блокировкой записи - накладные расходы на взятие любой блокировки намного больше чем проверка, чтобы увидеть, находится ли параметр вне диапазона, что просто выполнение этой проверки внутри блокировки записи, вероятно, будет работать лучше.
Джеймс
Этот класс также не кажется очень полезным, так как основанные на смещении функции (большинство из них) не могут быть использованы безопасно, если в любом случае не существует какой-либо схемы внешней блокировки, потому что коллекция может измениться между тем, когда вы решите, куда поместить или получить что-то от и когда вы действительно получите это.
Джеймс
1
Я хотел бы официально заявить, что признаю, что полезность IListсемантики в параллельных сценариях в лучшем случае ограничена. Я написал этот код, вероятно, до того, как пришел к этой реализации. Мой опыт такой же, как и у автора принятого ответа: я попробовал то, что я знал о синхронизации и IList <T>, и я кое-что узнал, сделав это.
Ронни Оверби