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

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

Одна вещь, которая кажется таинственно отсутствующей, - это ConcurrentList<T>. Должен ли я написать это сам (или получить его из Интернета:))?

Я пропустил что-то очевидное здесь?

Ответ 1

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

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

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

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

При всем уважении к большим предоставленным ответам, есть моменты, когда я просто хочу потокобезопасный IList. Ничего не продвинулось или не появилось. Производительность важна во многих случаях, но иногда это не вызывает беспокойства. Да, всегда есть проблемы без таких методов, как "TryGetValue" и т.д., Но в большинстве случаев я просто хочу что-то, что я могу перечислить, не беспокоясь о том, чтобы блокировать все вокруг. И да, кто-то, вероятно, может найти какую-то "ошибку" в моей реализации, которая может привести к тупику или чему-то (я полагаю), но давайте честно: когда дело доходит до многопоточности, если вы не правильно пишете свой код, все равно идет в тупик. Имея это в виду, я решил сделать простую реализацию ConcurrentList, которая обеспечивает эти основные потребности.

И для чего он стоит: я сделал базовый тест добавления 10 000 000 элементов в обычный список и 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
}

Ответ 4

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

Ответ 5

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

Ответ 6

В тех случаях, когда читает значительно больше, чем записи, или (как бы часто) записи не являются параллельными, может быть целесообразным подход copy-on-write.

Ниже приведена реализация

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

Для работы с копированием на запись вам необходимо постоянно сохранять свои структуры данных, т.е. никто не может изменять их после того, как вы сделали их доступными для других потоков. Когда вы хотите изменить, вы

  • клонировать структуру
  • внести изменения в клон
  • атомная замена в ссылке на модифицированный клон

код

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.

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

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

N.B. # 3 Если ваша структура данных огромна и вы часто ее изменяете, подход "копирование-все-на-запись" может быть запретительным как с точки зрения потребления памяти, так и из-за стоимости процессора при копировании. В этом случае вы можете вместо этого использовать MS Неизменяемые коллекции.

Ответ 7

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

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

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

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

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

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

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

Ответ 8

Некоторые люди подняли некоторые точки товара (и некоторые из моих мыслей):

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

Это не ответ. Это только комментарии, которые не подходят для определенного места.

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

Ответ 9

Я реализовал один, похожий на Brian's. Моя разница:

  • Я управляю массивом напрямую.
  • Я не вхожу в блокировки в блоке 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();
    }
}

Ответ 10

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

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

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