List.Add () bezpieczeństwo wątków


91

Rozumiem, że generalnie lista nie jest bezpieczna dla wątków, ale czy jest coś złego w zwykłym dodawaniu elementów do listy, jeśli wątki nigdy nie wykonują żadnych innych operacji na liście (takich jak przechodzenie przez nią)?

Przykład:

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

3
Dokładny duplikat bezpieczeństwa wątku List <T>
JK.

Kiedyś użyłem List <T> tylko do dodawania nowych obiektów z wielu równoległych zadań. Czasami, bardzo rzadko, kiedy iteracja listy po wykonaniu wszystkich zadań kończyła się rekordem, który był zerowy, co gdyby nie było żadnych dodatkowych wątków, byłoby to praktycznie niemożliwe. Wydaje mi się, że gdy lista wewnętrznie ponownie alokowała swoje elementy w celu rozwinięcia, w jakiś sposób inny wątek zepsuł ją, próbując dodać kolejny obiekt. Nie jest to więc dobry pomysł!
osmiumbin

Dokładnie to, co obecnie widzę @osmiumbin w odniesieniu do obiektu, który w niewytłumaczalny sposób jest zerowy, gdy po prostu dodaje się z wielu wątków. Dzięki za potwierdzenie.
Blackey

Odpowiedzi:


75

Za kulisami dzieje się wiele rzeczy, w tym ponowne przydzielanie buforów i kopiowanie elementów. Ten kod spowoduje niebezpieczeństwo. Po prostu nie ma żadnych niepodzielnych operacji podczas dodawania do listy, przynajmniej właściwość „Długość” musi zostać zaktualizowana, a element należy umieścić we właściwym miejscu, a (jeśli istnieje oddzielna zmienna) indeks potrzebuje do zaktualizowania. Wiele wątków może się nadepnąć. A jeśli wymagana jest uprawa, dzieje się o wiele więcej. Jeśli coś pisze na liście, nic innego nie powinno czytać ani pisać do niej.

W .NET 4.0 mamy współbieżne kolekcje, które są bezpieczne dla wątków i nie wymagają blokad.


To ma sens, na pewno przyjrzę się w tym celu nowym kolekcjom Concurrent. Dziękuję Ci.
e36M3

11
Zauważ, że nie ma wbudowanego ConcurrentListtypu. Istnieją równoczesne torby, słowniki, stosy, kolejki itp., Ale nie ma list.
LukeH

11

Twoje obecne podejście nie jest bezpieczne dla wątków - sugerowałbym całkowite unikanie tego - ponieważ zasadniczo wykonujesz transformację danych PLINQ może być lepszym podejściem (wiem, że to jest uproszczony przykład, ale na końcu projektujesz każdą transakcję w inny "stan ”obiekt).

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

Przedstawiłem zbyt uproszczony przykład, aby podkreślić aspekt List.Add, który mnie interesował. Mój Parallel.Foreach w rzeczywistości wykona sporo pracy i nie będzie prostą transformacją danych. Dzięki.
e36M3

4
kolekcje współbieżne mogą obniżyć wydajność równoległą, jeśli są niepotrzebne - inną rzeczą, którą możesz zrobić, jest użycie tablicy o stałym rozmiarze i Parallel.Foreachprzeciążenie, które pobiera indeks - w takim przypadku każdy wątek manipuluje innym wpisem tablicy i powinieneś być bezpieczny.
BrokenGlass

6

Nie jest to nierozsądne pytanie. Tam przypadki, w których metody, które mogą powodować problemy z wątku bezpieczeństwa w połączeniu z innymi metodami są bezpieczne, jeżeli są one tylko metoda nazywa.

Jednak ewidentnie tak nie jest, jeśli weźmiemy pod uwagę kod pokazany w reflektorze:

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

Nawet jeśli EnsureCapacitysam w sobie był wątkowo bezpieczny (a na pewno nie jest), powyższy kod z pewnością nie będzie bezpieczny wątkowo, biorąc pod uwagę możliwość jednoczesnego wywołania operatora przyrostu powodującego błędy w zapisie.

Albo zablokuj, użyj ConcurrentList, albo może użyj kolejki bez blokad jako miejsca, w którym zapisuje wiele wątków i odczytu z niej - bezpośrednio lub przez wypełnienie listy - po wykonaniu swojej pracy (zakładam, że wielokrotne jednoczesne zapisy, po których następuje odczyt jednowątkowy, jest tutaj twoim wzorcem, sądząc po twoim pytaniu, ponieważ w przeciwnym razie nie widzę, jak warunek, w którym Addjest jedyną wywołaną metodą, mógłby się przydać).


6

Jeśli chcesz używać List.addz wielu wątków i nie dbasz o kolejność, prawdopodobnie i tak nie potrzebujesz możliwości indeksowania Listi zamiast tego powinieneś użyć niektórych dostępnych kolekcji współbieżnych.

Jeśli zignorujesz tę radę i tylko to zrobisz add, możesz sprawić, że addwątek będzie bezpieczny, ale w nieprzewidywalnej kolejności:

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

...

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

Jeśli zaakceptujesz tę nieprzewidywalną kolejność, prawdopodobnie, jak wspomniano wcześniej, nie potrzebujesz kolekcji, która może wykonywać indeksowanie jak w someList[i].


5

Spowodowałoby to problemy, ponieważ lista jest zbudowana na tablicy i nie jest bezpieczna dla wątków, możesz uzyskać indeks poza granicami wyjątku lub niektóre wartości przesłaniają inne wartości, w zależności od tego, gdzie znajdują się wątki. Zasadniczo nie rób tego.

Istnieje wiele potencjalnych problemów ... Po prostu nie rób tego. Jeśli potrzebujesz kolekcji bezpiecznej wątkowo, użyj blokady lub jednej z kolekcji System.Collections.Concurrent.



2

Czy jest coś złego w zwykłym dodawaniu elementów do listy, jeśli wątki nigdy nie wykonują żadnych innych operacji na liście?

Krótka odpowiedź: tak.

Długa odpowiedź: uruchom poniższy program.

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 czy uruchamiasz to na komputerze jednordzeniowym?
Bas Smit

Cześć, myślę, że jest problem z tym przykładem, ponieważ ustawia AutoResetEvent za każdym razem, gdy znajdzie liczbę 1000. Ponieważ może przetwarzać te wątki, kiedy tylko zechce, może osiągnąć 1000, zanim na przykład osiągnie 999. Jeśli dodasz Console.WriteLine w metodzie AddTol, zobaczysz, że numeracja nie jest w porządku.
Dave Walker

@dave, ustawia wydarzenie, gdy i == 0
Bas Smit

2

Jak już powiedzieli inni, możesz używać kolekcji współbieżnych z System.Collections.Concurrentprzestrzeni nazw. Jeśli możesz użyć jednego z nich, jest to preferowane.

Ale jeśli naprawdę potrzebujesz listy, która jest właśnie zsynchronizowana, możesz spojrzeć na SynchronizedCollection<T>-Class w System.Collections.Generic.

Zauważ, że musiałeś dołączyć zestaw System.ServiceModel, co jest również powodem, dla którego nie lubię go tak bardzo. Ale czasami go używam.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.