List.Add () thread safety


91

Capisco che in generale un elenco non è thread-safe, tuttavia c'è qualcosa di sbagliato nell'aggiungere semplicemente elementi in un elenco se i thread non eseguono mai altre operazioni nell'elenco (come attraversarlo)?

Esempio:

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

3
Esatto duplicato di List <T> thread safety
JK.

Una volta ho usato un List <T> solo per aggiungere nuovi oggetti da più attività eseguite in parallelo. A volte, molto raro, quando si scorre l'elenco dopo che tutte le attività sono state completate, si ottiene un record nullo, il che, se non fossero stati coinvolti thread aggiuntivi, sarebbe stato praticamente impossibile che ciò accadesse. Immagino che, quando l'elenco stava riassegnando internamente i suoi elementi da espandere, in qualche modo un altro thread lo abbia incasinato provando ad aggiungere un altro oggetto. Quindi non è una buona idea farlo!
osmiumbin

Esattamente quello che sto vedendo attualmente @osmiumbin per quanto riguarda un oggetto che inspiegabilmente è nullo quando si aggiunge semplicemente da più thread. Grazie per la conferma.
Blackey

Risposte:


75

Dietro le quinte accadono molte cose, inclusa la riallocazione dei buffer e la copia di elementi. Quel codice causerà pericolo. Molto semplicemente, non ci sono operazioni atomiche quando si aggiunge a un elenco, almeno la proprietà "Lunghezza" deve essere aggiornata e l'elemento deve essere inserito nella posizione corretta e (se c'è una variabile separata) l'indice deve da aggiornare. Più thread possono calpestarsi l'uno sull'altro. E se è necessaria una crescita, c'è molto altro da fare. Se qualcosa sta scrivendo in un elenco, nient'altro dovrebbe leggerlo o scriverlo.

In .NET 4.0 abbiamo raccolte simultanee, che sono facilmente threadsafe e non richiedono blocchi.


Ha perfettamente senso, per questo guarderò sicuramente le nuove collezioni Concurrent. Grazie.
e36M3

11
Nota che non esiste un tipo integrato ConcurrentList. Ci sono borse, dizionari, pile, code ecc. Simultanee, ma non elenchi.
LukeH

11

Il tuo approccio attuale non è thread-safe - ti suggerirei di evitarlo del tutto - dal momento che fondamentalmente fai una trasformazione dei dati PLINQ potrebbe essere un approccio migliore (so che questo è un esempio semplificato ma alla fine stai proiettando ogni transazione in un altro "stato "oggetto).

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

Ho presentato un esempio eccessivamente semplificato per sottolineare l'aspetto di List.Add che mi interessava. My Parallel.Foreach infatti farà una buona mole di lavoro e non sarà una semplice trasformazione dei dati. Grazie.
e36M3

4
raccolte simultanee possono paralizzare le prestazioni parallele se utilizzate non necessarie - un'altra cosa che puoi fare è utilizzare un array di dimensioni fisse e utilizzare l' Parallel.Foreachoverload che accetta l'indice - in quel caso ogni thread sta manipolando una voce di array diversa e dovresti essere al sicuro.
BrokenGlass

6

Non è una cosa irragionevole da chiedere. Ci sono casi in cui i metodi che possono causare problemi di thread safety in combinazione con altri metodi sono sicuri se sono l'unico metodo chiamato.

Tuttavia, questo chiaramente non è un caso, se si considera il codice mostrato in reflector:

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

Anche se EnsureCapacityfosse di per sé threadsafe (e certamente non lo è), il codice sopra chiaramente non lo sarà, considerando la possibilità che chiamate simultanee all'operatore di incremento causino errori di scrittura.

Blocca, usa ConcurrentList, o forse usa una coda priva di blocchi come luogo in cui scrivono più thread e leggi da esso - direttamente o riempiendo un elenco - dopo che hanno svolto il loro lavoro (presumo che più scritture simultanee seguite da lettura a thread singolo è il tuo modello qui, a giudicare dalla tua domanda, altrimenti non riesco a vedere come la condizione in cui si Addtrova l'unico metodo chiamato potrebbe essere di qualche utilità).


6

Se desideri utilizzare List.addda più thread e non ti interessa l'ordine, probabilmente non hai bisogno della capacità di indicizzazione di un Listcomunque, e dovresti invece usare alcune delle raccolte simultanee disponibili.

Se ignori questo consiglio e lo fai solo add, potresti rendere addthread safe ma in un ordine imprevedibile come questo:

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

...

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

Se accetti questo ordine imprevedibile, è probabile che, come accennato in precedenza, non hai bisogno di una raccolta che possa eseguire l'indicizzazione come in someList[i].


5

Ciò causerebbe problemi, poiché l'elenco è costruito su un array e non è thread-safe, potresti ottenere un'eccezione di indice fuori dai limiti o alcuni valori che sovrascrivono altri valori, a seconda di dove si trovano i thread. Fondamentalmente, non farlo.

Ci sono molteplici potenziali problemi ... Non farlo. Se è necessaria una raccolta thread-safe, utilizzare un lock o una delle raccolte System.Collections.Concurrent.


5

Ho risolto il mio problema usando ConcurrentBag<T>invece di List<T>così:

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

2

C'è qualcosa di sbagliato nella semplice aggiunta di elementi in un elenco se i thread non eseguono mai altre operazioni nell'elenco?

Risposta breve: sì.

Risposta lunga: esegui il programma di seguito.

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 stai eseguendo questo su una macchina single core?
Bas Smit

Ciao, penso che ci sia un problema con questo esempio poiché imposta AutoResetEvent ogni volta che trova il numero 1000. Poiché può elaborare questi thread un po 'ogni volta che lo desidera, può arrivare a 1000 prima che arrivi a 999, ad esempio. Se aggiungi Console.WriteLine nel metodo AddTol, vedrai che la numerazione non è in ordine.
Dave Walker

@ dave, sta impostando l'evento quando i == 0
Bas Smit

2

Come altri hanno già detto, puoi usare raccolte simultanee dallo System.Collections.Concurrentspazio dei nomi. Se puoi usare uno di questi, questo è il preferito.

Ma se vuoi davvero una lista che sia appena sincronizzata, potresti guardare la SynchronizedCollection<T>-Class in System.Collections.Generic.

Nota che dovevi includere l'assembly System.ServiceModel, che è anche il motivo per cui non mi piace così tanto. Ma a volte lo uso.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.