Безопасность List.Add()

Я понимаю, что в целом список не является потокобезопасным, однако есть ли что-то неправильное в простом добавлении элементов в список, если потоки никогда не выполняют каких-либо других операций в списке (например, пересекают его)?

Пример:

List<object> list = new List<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});

Ответ 1

За кулисами происходит много вещей, в том числе перераспределение буферов и копирующих элементов. Этот код вызовет опасность. Очень просто, при добавлении в список нет никаких атомных операций, по крайней мере, для свойства "Длина" должны быть обновления, а элемент должен быть помещен в нужное место и (если есть отдельная переменная), потребность в индексе быть обновленным. Несколько потоков могут попирать друг друга. И если требуется рост, тогда происходит еще много. Если что-то пишется в список, ничто не должно читать или писать ему.

В .NET 4.0 у нас есть параллельные коллекции, которые удобны для потокобезопасности и не требуют блокировок.

Ответ 2

Текущий подход не является потокобезопасным - я бы предложил избегать этого вообще - так как вы в основном выполняете преобразование данных PLINQ может быть лучшим подходом (я знаю, что это упрощенный пример, но в конце вы проектируете каждую транзакцию в другой объект "state" ).

List<object> list = transactions.AsParallel()
                                .Select( tran => new object())
                                .ToList();

Ответ 3

Это не необоснованная вещь. Бывают случаи, когда методы, которые могут вызывать проблемы безопасности потоков в сочетании с другими методами, являются безопасными, если они являются единственным методом.

Однако это явно не относится к этому случаю, когда вы рассматриваете код, показанный в рефлекторе:

public void Add(T item)
{
    if (this._size == this._items.Length)
    {
        this.EnsureCapacity(this._size + 1);
    }
    this._items[this._size++] = item;
    this._version++;
}

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

Либо заблокируйте, либо используйте ConcurrentList, либо, возможно, используйте незаблокированную очередь, как место, в которое записывается множество потоков, и чтение из него - либо напрямую, либо путем заполнения списка с ним - после того, как они выполнили свою работу (I ' m, предполагая, что несколько одновременных записей, за которыми следует однопоточное чтение, является вашим шаблоном здесь, судя по вашему вопросу, так как в противном случае я не вижу, как может быть использовано условие, когда Add - единственный метод, который может быть использован).

Ответ 4

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

Есть много потенциальных проблем... Просто не надо. Если вам нужна потокобезопасная коллекция, используйте блокировку или одну из коллекций System.Collections.Concurrent.

Ответ 5

Есть ли что-то не так, просто добавляя элементы в список, если потоки никогда не выполняют никаких других операций в списке?

Короткий ответ: да.

Длинный ответ: запустите программу ниже.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

class Program
{
    readonly List<int> l = new List<int>();
    const int amount = 1000;
    int toFinish = amount;
    readonly AutoResetEvent are = new AutoResetEvent(false);

    static void Main()
    {
        new Program().Run();
    }

    void Run()
    {
        for (int i = 0; i < amount; i++)
            new Thread(AddTol).Start(i);

        are.WaitOne();

        if (l.Count != amount ||
            l.Distinct().Count() != amount ||
            l.Min() < 0 ||
            l.Max() >= amount)
            throw new Exception("omg corrupted data");

        Console.WriteLine("All good");
        Console.ReadKey();
    }

    void AddTol(object o)
    {
        // uncomment to fix
        // lock (l) 
        l.Add((int)o);

        int i = Interlocked.Decrement(ref toFinish);

        if (i == 0)
            are.Set();
    }
}

Ответ 6

Как уже говорили другие, вы можете использовать параллельные коллекции из пространства имен System.Collections.Concurrent. Если вы можете использовать один из них, это предпочтительнее.

Но если вам действительно нужен список, который просто синхронизирован, вы можете посмотреть SynchronizedCollection<T> -Class в System.Collections.Generic.

Обратите внимание, что вам нужно включить сборку System.ServiceModel, что также является причиной, по которой мне это не нравится. Но иногда я использую его.

Ответ 7

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

Если вы проигнорируете этот совет и выполняете add, вы можете сделать поток add потокобезопасным, но в непредсказуемом порядке:

private Object someListLock = new Object(); // only once

...

lock (someListLock)
{
    someList.Add(item);
}

Если вы примете этот непредсказуемый заказ, скорее всего, вам, как упоминалось ранее, не нужна коллекция, которая может индексировать, как в someList[i].