List.Add () безпека потоку


87

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

Приклад:

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


Одного разу я використовував List <T> лише для додавання нових об’єктів із декількох паралельно виконуваних завдань. Іноді, дуже рідко, коли ітерація списку після завершення всіх завдань закінчувалася записом, який був нульовим, що, якби не були задіяні зайві потоки, це було б практично неможливо для цього. Я припускаю, що коли список внутрішньо перерозподіляв свої елементи для розширення, якось інший потік зіпсував його, намагаючись додати інший об'єкт. Тож це не гарна ідея робити це!
осміумбін

Саме те, що я зараз бачу @osmiumbin, щодо об’єкта, який незрозуміло є нульовим при простому додаванні з декількох потоків. Дякуємо за підтвердження.
Blackey

Відповіді:


75

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

У .NET 4.0 ми маємо одночасні колекції, які легко безпечні для потоків і не потребують блокування.


Це цілком логічно, я обов’язково розгляну для цього нові колекції Concurrent. Дякую.
e36M3,

11
Зверніть увагу, що вбудованого ConcurrentListтипу немає. Є паралельні пакети, словники, стеки, черги тощо, але списків немає.
LukeH

11

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

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

Я представив надто спрощений приклад, щоб підкреслити аспект List.Додайте, що мене цікавив. Мій Parallel.Foreach насправді виконає великий обсяг роботи і не буде простим перетворенням даних. Дякую.
e36M3,

4
паралельні колекції можуть зіпсувати вашу паралельну продуктивність, якщо вони використовуються непотрібно - ще одна річ, яку ви можете зробити, - це використовувати масив фіксованого розміру та використовувати Parallel.Foreachперевантаження, що враховує індекс - у цьому випадку кожен потік маніпулює іншим записом масиву, і ви повинні бути в безпеці.
BrokenGlass

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

Або заблокуйте, використовуйте 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());
});

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 ти використовуєш це на одноядерній машині?
Бас Сміт

Привіт, я думаю, у цьому прикладі є проблема, оскільки він встановлює AutoResetEvent щоразу, коли знаходить число 1000. Оскільки він може обробляти ці потоки в будь-який час, коли захоче, може дійти до 1000, перш ніж дійде до 999, наприклад. Якщо ви додасте Console.WriteLine в методі AddTol, ви побачите, що нумерація не в порядку.
Dave Walker

@dave, встановлення події, коли i == 0
Бас Сміт

2

Як вже говорили інші, ви можете використовувати одночасні колекції з System.Collections.Concurrentпростору імен. Якщо ви можете скористатися одним із них, це переважно.

Але якщо вам дійсно потрібен список, який просто синхронізований, ви можете подивитися на SynchronizedCollection<T>-Class в System.Collections.Generic.

Зверніть увагу, що вам довелося включити збірку System.ServiceModel, що також є причиною того, чому мені це не так подобається. Але іноді я цим користуюся.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.