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

87

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

Пример:

List<object> list = new List<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});
e36M3
источник
3
Точный дубликат потоковой безопасности List <T>
JK.
Однажды я использовал List <T> только для добавления новых объектов из нескольких параллельно выполняемых задач. Иногда, очень редко, при итерации по списку после того, как все задачи были завершены, это приводило к нулевой записи, что, если бы не было задействовано дополнительных потоков, это было бы практически невозможно. Я предполагаю, что, когда список внутренне перераспределял свои элементы для расширения, каким-то образом другой поток испортил его, пытаясь добавить еще один объект. Так что это не лучшая идея!
осмийбин
Именно то, что я сейчас вижу @osmiumbin в отношении объекта, необъяснимо являющегося нулевым при простом добавлении из нескольких потоков. Спасибо за подтверждение.
Блэки

Ответы:

75

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

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

Talljoe
источник
В этом есть смысл, я обязательно посмотрю на новые коллекции Concurrent для этого. Спасибо.
e36M3 08
11
Обратите внимание, что встроенного ConcurrentListтипа нет. Существуют параллельные пакеты, словари, стопки, очереди и т. Д., Но нет списков.
LukeH
11

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

List<object> list = transactions.AsParallel()
                                .Select( tran => new object())
                                .ToList();
Разбитое стекло
источник
Я представил слишком упрощенный пример, чтобы подчеркнуть аспект List.Add, который меня интересовал. My Parallel.Foreach на самом деле будет делать много работы и не будет простым преобразованием данных. Спасибо.
e36M3 08
4
параллельные коллекции могут снизить вашу параллельную производительность, если они не используются - еще одна вещь, которую вы можете сделать, это использовать массив фиксированного размера и использовать Parallel.Foreachперегрузку, которая принимает индекс - в этом случае каждый поток управляет другой записью массива, и вы должны быть в безопасности.
BrokenGlass 08
6

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

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

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, либо, возможно, используйте очередь без блокировки в качестве места, куда несколько потоков записывают и читают из него - либо напрямую, либо путем заполнения им списка - после того, как они сделали свою работу (я предполагаю, что несколько одновременных записей с последующим однопоточным чтением - вот ваш образец здесь, судя по вашему вопросу, поскольку в противном случае я не могу понять, как условие, в котором Addвызывается единственный вызываемый метод, может быть использовано).

Джон Ханна
источник
6

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

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

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

...

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

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

Томсв
источник
5

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

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

Линкгорон
источник
5

Я решил свою проблему, используя ConcurrentBag<T>вместо List<T>этого:

ConcurrentBag<object> list = new ConcurrentBag<object>();
Parallel.ForEach(transactions, tran =>
{
    list.Add(new object());
});
Alansiqueira27
источник
2

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

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

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

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();
    }
}
Бас Смит
источник
@royi, вы запускаете это на одноядерной машине?
Bas Smit
Привет, я думаю, что в этом примере есть проблема, так как он устанавливает AutoResetEvent всякий раз, когда находит число 1000. Поскольку он может обрабатывать эти потоки как бы всякий раз, когда он хочет, он может достичь 1000, прежде чем, например, достигнет 999. Если вы добавите Console.WriteLine в метод AddTol, вы увидите, что нумерация не в порядке.
Дэйв Уокер
@dave, он устанавливает событие, когда i == 0
Bas Smit
2

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

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

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

маф-софт
источник