Nessun ConcurrentList <T> in .Net 4.0?


198

Ero entusiasta di vedere il nuovo System.Collections.Concurrentspazio dei nomi in .Net 4.0, abbastanza bello! Ho visto ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBage BlockingCollection.

Una cosa che sembra misteriosamente mancante è a ConcurrentList<T>. Devo scriverlo da solo (o toglierlo dal web :))?

Mi sto perdendo qualcosa di ovvio qui?



4
@RodrigoReis, ConcurrentBag <T> è una raccolta non ordinata, mentre la Lista <T> è ordinata.
Adam Calvet Bohl,

4
Come è possibile avere una raccolta ordinata in un ambiente multithread? Non avresti mai il controllo della sequenza di elementi, in base alla progettazione.
Jeremy Holovacs,

Usa invece un lucchetto
Erik Bergstedt,

c'è un file chiamato ThreadSafeList.cs nel codice sorgente dotnet che assomiglia molto al codice seguente. Utilizza anche ReaderWriterLockSlim e stava cercando di capire perché usarlo invece del semplice blocco (obj)?
Colin Lamarre,

Risposte:


166

Ci ho provato qualche tempo fa (anche: su GitHub ). La mia implementazione ha avuto alcuni problemi, che non entrerò qui. Lascia che ti dica, soprattutto, cosa ho imparato.

In primo luogo, non è possibile ottenere un'implementazione completa di IList<T>questo senza blocco e thread-safe. In particolare, gli inserimenti e le rimozioni casuali non funzioneranno, a meno che non dimentichi anche l'accesso casuale O (1) (cioè, a meno che tu non "imbroglia" e usi semplicemente una sorta di elenco collegato e lasci che l'indicizzazione faccia schifo).

Quello che ho pensato che potrebbe essere utile è stato un thread-safe, sottoinsieme limitato di IList<T>: in particolare, quella che avrebbe permesso una Adde fornire casuali di sola lettura l'accesso da parte di indice (ma no Insert, RemoveAte così via, e anche non casuale in scrittura accesso).

Questo era l'obiettivo della mia ConcurrentList<T>implementazione . Ma quando ho testato le sue prestazioni in scenari multithread, ho scoperto che semplicemente sincronizzare l'aggiunta con una List<T>era più veloce . Fondamentalmente, l'aggiunta a a List<T>è già velocissima; la complessità dei passaggi computazionali coinvolti è minuscola (incrementa un indice e assegna a un elemento in un array; tutto qui ). Avresti bisogno di un sacco di scritture simultanee per vedere qualsiasi tipo di contesa di blocco su questo; e anche in questo caso, le prestazioni medie di ogni scrittura eliminerebbero comunque l'implementazione più costosa sebbene senza lock in ConcurrentList<T>.

Nel caso relativamente raro che l'array interno dell'elenco debba ridimensionarsi, si paga un piccolo costo. Quindi alla fine ho concluso che questo era uno scenario di nicchia in cui ConcurrentList<T>avrebbe avuto senso un tipo di raccolta solo aggiunta : quando si desidera garantire un basso sovraccarico di aggiunta di un elemento su ogni singola chiamata (quindi, al contrario di un obiettivo di prestazione ammortizzato).

Semplicemente non è affatto utile una classe come si potrebbe pensare.


52
E se avete bisogno di qualcosa di simile a List<T>che usi vecchia scuola, la sincronizzazione del monitor-based, c'è SynchronizedCollection<T>nascosto nella BCL: msdn.microsoft.com/en-us/library/ms668265.aspx
LukeH

8
Una piccola aggiunta: utilizzare il parametro Costruttore capacità per evitare (per quanto possibile) lo scenario di ridimensionamento.
Henk Holterman,

2
Lo scenario più grande in cui una ConcurrentListsarebbe una vittoria sarebbe quando non ci sono molte attività che si aggiungono all'elenco, ma ci sono molti lettori simultanei. Si potrebbe ridurre il sovraccarico dei lettori a una singola barriera di memoria (ed eliminarlo anche se i lettori non fossero preoccupati per i dati leggermente obsoleti).
supercat,

2
@Kevin: È abbastanza banale costruirne uno ConcurrentList<T>in modo tale che i lettori siano sicuri di vedere uno stato coerente senza bisogno di alcun blocco, con un sovraccarico relativamente leggero aggiunto. Quando l'elenco si espande, ad esempio dalle dimensioni 32 a 64, mantenere l'array size-32 e creare un nuovo array size-64. Quando si aggiungono ciascuno dei successivi 32 elementi, inserirlo nello slot un 32-63 del nuovo array e copiare un vecchio elemento dall'array size-32 a quello nuovo. Fino all'aggiunta del 64 ° elemento, i lettori cercheranno nell'array size-32 gli articoli 0-31 e nell'array size-64 gli articoli 32-63.
Supercat,

2
Una volta aggiunto il 64 ° elemento, l'array size-32 continuerà a funzionare per recuperare gli elementi da 0 a 31, ma i lettori non dovranno più utilizzarlo. Possono utilizzare l'array size-64 per tutti gli articoli 0-63 e un array size-128 per gli articoli 64-127. Il sovraccarico di selezionare quale dei due array utilizzare, oltre a una barriera di memoria, se lo si desidera, sarebbe inferiore al sovraccarico anche del blocco lettore-scrittore più efficiente che si possa immaginare. Le scritture dovrebbero probabilmente usare i blocchi (senza lock sarebbe possibile, specialmente se non ci si preoccupasse di creare una nuova istanza di oggetto ad ogni inserimento, ma il blocco dovrebbe essere economico.
Supercat

38

Per cosa useresti una ConcurrentList?

Il concetto di contenitore ad accesso casuale in un mondo thread non è così utile come potrebbe apparire. La dichiarazione

  if (i < MyConcurrentList.Count)  
      x = MyConcurrentList[i]; 

nel suo insieme non sarebbe ancora sicuro per i thread.

Invece di creare una ConcurrentList, prova a creare soluzioni con quello che c'è. Le classi più comuni sono ConcurrentBag e soprattutto BlockingCollection.


Buon punto. Comunque quello che sto facendo è un po 'più banale. Sto solo cercando di assegnare ConcurrentBag <T> a un IList <T>. Potrei passare la mia proprietà a un IEnumerable <T>, ma poi non riesco ad aggiungere altro.
Alan,

1
@Alan: Non c'è modo di implementarlo senza bloccare l'elenco. Dato che puoi già utilizzarlo Monitorper farlo comunque, non c'è motivo per un elenco simultaneo.
Billy ONeal,

6
@dcp - sì, questo è intrinsecamente non thread-safe. ConcurrentDictionary ha metodi speciali che lo fanno in un'operazione atomica, come AddOrUpdate, GetOrAdd, TryUpdate, ecc. Hanno ancora ContainsKey perché a volte vuoi solo sapere se la chiave è lì senza modificare il dizionario (pensa HashSet)
Zarat

3
@dcp - ContainsKey è un thread-safe di per sé, il tuo esempio (non ContainsKey!) ha solo una condizione di gara perché fai una seconda chiamata a seconda della prima decisione, che a quel punto potrebbe essere già obsoleta.
Zarat,

2
Henk, non sono d'accordo. Penso che ci sia uno scenario semplice in cui potrebbe essere molto utile. Il thread di lavoro in cui scrive il thread dell'interfaccia utente leggerà e aggiornerà l'interfaccia di conseguenza. Se si desidera aggiungere l'elemento in modo ordinato, sarà necessario scrivere in modo casuale. Puoi anche usare uno stack e una vista per i dati ma dovrai mantenere 2 raccolte :-(.
Eric Ouellet,

19

Con tutto il dovuto rispetto per le grandi risposte già fornite, ci sono volte in cui voglio semplicemente un IList thread-safe. Niente di avanzato o di fantasia. Le prestazioni sono importanti in molti casi, ma a volte questo non è un problema. Sì, ci saranno sempre sfide senza metodi come "TryGetValue", ecc., Ma nella maggior parte dei casi voglio solo qualcosa che posso enumerare senza dovermi preoccupare di mettere i blocchi intorno a tutto. E sì, qualcuno può probabilmente trovare qualche "bug" nella mia implementazione che potrebbe portare a un deadlock o qualcosa del genere (suppongo) ma siamo onesti: quando si tratta di multi-threading, se non si scrive correttamente il codice, esso sta andando comunque a un punto morto. Con questo in mente, ho deciso di realizzare una semplice implementazione ConcurrentList che fornisce queste esigenze di base.

E per quello che vale: ho fatto un test di base per aggiungere 10.000.000 di articoli a List e ConcurrentList regolari e i risultati sono stati:

Elenco completato in: 7793 millisecondi. Concorrente finito in: 8064 millisecondi.

public class ConcurrentList<T> : IList<T>, IDisposable
{
    #region Fields
    private readonly List<T> _list;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructors
    public ConcurrentList()
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>();
    }

    public ConcurrentList(int capacity)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(capacity);
    }

    public ConcurrentList(IEnumerable<T> items)
    {
        this._lock = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);
        this._list = new List<T>(items);
    }
    #endregion

    #region Methods
    public void Add(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Add(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void Insert(int index, T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Insert(index, item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Remove(T item)
    {
        try
        {
            this._lock.EnterWriteLock();
            return this._list.Remove(item);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public void RemoveAt(int index)
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.RemoveAt(index);
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public int IndexOf(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.IndexOf(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void Clear()
    {
        try
        {
            this._lock.EnterWriteLock();
            this._list.Clear();
        }
        finally
        {
            this._lock.ExitWriteLock();
        }
    }

    public bool Contains(T item)
    {
        try
        {
            this._lock.EnterReadLock();
            return this._list.Contains(item);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        try
        {
            this._lock.EnterReadLock();
            this._list.CopyTo(array, arrayIndex);
        }
        finally
        {
            this._lock.ExitReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return new ConcurrentEnumerator<T>(this._list, this._lock);
    }

    ~ConcurrentList()
    {
        this.Dispose(false);
    }

    public void Dispose()
    {
        this.Dispose(true);
    }

    private void Dispose(bool disposing)
    {
        if (disposing)
            GC.SuppressFinalize(this);

        this._lock.Dispose();
    }
    #endregion

    #region Properties
    public T this[int index]
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list[index];
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
        set
        {
            try
            {
                this._lock.EnterWriteLock();
                this._list[index] = value;
            }
            finally
            {
                this._lock.ExitWriteLock();
            }
        }
    }

    public int Count
    {
        get
        {
            try
            {
                this._lock.EnterReadLock();
                return this._list.Count;
            }
            finally
            {
                this._lock.ExitReadLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return false; }
    }
    #endregion
}

    public class ConcurrentEnumerator<T> : IEnumerator<T>
{
    #region Fields
    private readonly IEnumerator<T> _inner;
    private readonly ReaderWriterLockSlim _lock;
    #endregion

    #region Constructor
    public ConcurrentEnumerator(IEnumerable<T> inner, ReaderWriterLockSlim @lock)
    {
        this._lock = @lock;
        this._lock.EnterReadLock();
        this._inner = inner.GetEnumerator();
    }
    #endregion

    #region Methods
    public bool MoveNext()
    {
        return _inner.MoveNext();
    }

    public void Reset()
    {
        _inner.Reset();
    }

    public void Dispose()
    {
        this._lock.ExitReadLock();
    }
    #endregion

    #region Properties
    public T Current
    {
        get { return _inner.Current; }
    }

    object IEnumerator.Current
    {
        get { return _inner.Current; }
    }
    #endregion
}

5
OK, vecchia risposta ma ancora: RemoveAt(int index)non è mai thread-safe, Insert(int index, T item)è sicuro solo per index == 0, il ritorno di IndexOf()è immediatamente obsoleto ecc. Non iniziare nemmeno con this[int].
Henk Holterman,

2
E non hai bisogno e non vuoi un ~ Finalizer ().
Henk Holterman,

2
Dici di aver rinunciato a prevenire la possibilità di deadlock e che un singolo ReaderWriterLockSlimpuò essere reso facilmente deadlock usando EnterUpgradeableReadLock()contemporaneamente. Tuttavia non lo usi, non rendi il blocco accessibile dall'esterno, e ad esempio non chiami un metodo che inserisce un blocco di scrittura mentre tieni premuto un blocco di lettura, quindi l'uso della classe non crea più deadlock probabile.
Eugene Beresovsky,

1
L'interfaccia non simultanea non è appropriata per l'accesso simultaneo. Ad esempio, il seguente non è atomico var l = new ConcurrentList<string>(); /* ... */ l[0] += "asdf";. In generale, qualsiasi combinazione di lettura / scrittura può causare problemi profondi se eseguita contemporaneamente. Ecco perché strutture di dati simultanei generalmente forniscono metodi per chi, come ConcurrentDictionary's AddOrGetecc NB Your costante (e ridondante in quanto i membri sono già contrassegnati come tali dalla sottolineatura) ripetizione delle this.ingombra.
Eugene Beresovsky,

1
Grazie Eugenio. Sono un grande utente di .NET Reflector che mette "questo". su tutti i campi non statici. Come tale, sono cresciuto per preferire lo stesso. Considerando che questa interfaccia non concorrente non è appropriata: hai assolutamente ragione nel provare a eseguire più azioni contro la mia implementazione può diventare inaffidabile. Ma il requisito qui è semplicemente che le singole azioni (aggiungi, rimuovi, cancella o enumerazione) possono essere eseguite senza corrompere la raccolta. Rimuove sostanzialmente la necessità di mettere le dichiarazioni di blocco intorno a tutto.
Brian Booth,

11

ConcurrentList(come un array ridimensionabile, non un elenco collegato) non è facile da scrivere con operazioni non bloccanti. La sua API non si traduce bene in una versione "concorrente".


12
Non è solo difficile da scrivere, è anche difficile capire un'interfaccia utile.
CodesInChaos,

11

Il motivo per cui non esiste una ConcurrentList è perché fondamentalmente non può essere scritto. Il motivo è che diverse operazioni importanti in IList si basano su indici e che semplicemente non funzionerà. Per esempio:

int catIndex = list.IndexOf("cat");
list.Insert(catIndex, "dog");

L'effetto che l'autore sta cercando è di inserire "cane" prima di "gatto", ma in un ambiente multithread, tutto può succedere all'elenco tra quelle due righe di codice. Ad esempio, potrebbe fare un altro thread list.RemoveAt(0), spostando l'intero elenco a sinistra, ma soprattutto, catIndex non cambierà. L'impatto qui è che l' Insertoperazione metterà effettivamente il "cane" dopo il gatto, non prima di esso.

Le diverse implementazioni che vedi offerte come "risposte" a questa domanda sono ben intenzionate, ma come mostra quanto sopra, non offrono risultati affidabili. Se vuoi davvero una semantica simile a una lista in un ambiente multithread, non puoi arrivarci inserendo i blocchi all'interno dei metodi di implementazione della lista. Devi assicurarti che qualsiasi indice che usi risieda interamente nel contesto del blocco. Il risultato è che puoi usare una Lista in un ambiente multithread con il giusto blocco, ma la lista stessa non può essere fatta esistere in quel mondo.

Se pensi di aver bisogno di un elenco simultaneo, ci sono davvero solo due possibilità:

  1. Ciò di cui hai veramente bisogno è un ConcurrentBag
  2. Devi creare la tua raccolta, magari implementata con un elenco e il tuo controllo di concorrenza.

Se hai un ConcurrentBag e sei in una posizione in cui devi passarlo come IList, allora hai un problema, perché il metodo che stai chiamando ha specificato che potrebbero provare a fare qualcosa come ho fatto sopra con il gatto & cane. Nella maggior parte dei mondi, ciò significa che il metodo che stai chiamando non è semplicemente progettato per funzionare in un ambiente multi-thread. Ciò significa che lo rifattrai in modo che sia o, se non puoi, dovrai gestirlo con molta attenzione. Quasi sicuramente ti verrà richiesto di creare la tua raccolta con i suoi blocchi e chiamare il metodo offensivo all'interno di un blocco.


5

Nei casi in cui le letture superano di molto le scritture o le scritture (per quanto frequenti) non siano simultanee , può essere appropriato un approccio di copia su scrittura .

L'implementazione mostrata di seguito è

  • lockless
  • incredibilmente veloce per letture simultanee , anche mentre sono in corso modifiche simultanee - non importa quanto tempo impiegano
  • perché gli "snapshot" sono immutabili, l'atomicità senza blocco è possibile, cioè var snap = _list; snap[snap.Count - 1];non verrà mai (beh, tranne per un elenco vuoto ovviamente), e otterrai anche un elenco thread-safe con semantica di snapshot gratis .. come ADORO l'immutabilità!
  • implementato genericamente , applicabile a qualsiasi struttura di dati e qualsiasi tipo di modifica
  • semplice , cioè facile da testare, eseguire il debug, verificare leggendo il codice
  • utilizzabile in .Net 3.5

Affinché copy-on-write funzioni, è necessario mantenere le strutture dei dati effettivamente immutabili , ovvero nessuno è autorizzato a modificarle dopo averle rese disponibili per altri thread. Quando vuoi modificare, tu

  1. clonare la struttura
  2. apportare modifiche al clone
  3. scambiare atomicamente il riferimento al clone modificato

Codice

static class CopyOnWriteSwapper
{
    public static void Swap<T>(ref T obj, Func<T, T> cloner, Action<T> op)
        where T : class
    {
        while (true)
        {
            var objBefore = Volatile.Read(ref obj);
            var newObj = cloner(objBefore);
            op(newObj);
            if (Interlocked.CompareExchange(ref obj, newObj, objBefore) == objBefore)
                return;
        }
    }
}

uso

CopyOnWriteSwapper.Swap(ref _myList,
    orig => new List<string>(orig),
    clone => clone.Add("asdf"));

Se hai bisogno di maggiori prestazioni, ti aiuterà a non rigenerare il metodo, ad es. Crea un metodo per ogni tipo di modifica (Aggiungi, Rimuovi, ...) che desideri, e codifica i puntatori di funzione clonere op.

NB # 1 È responsabilità dell'utente assicurarsi che nessuno modifichi la struttura (presumibilmente) immutabile dei dati. Non c'è nulla che possiamo fare in un'implementazione generica per evitarlo , ma quando ti specializzi a List<T>, potresti proteggerti dalle modifiche usando List.AsReadOnly ()

NB # 2 Prestare attenzione ai valori nell'elenco. L'approccio copy on write sopra protegge solo l'appartenenza all'elenco, ma se non inserissi stringhe, ma alcuni altri oggetti mutabili, devi occuparti della sicurezza del thread (ad esempio il blocco). Ma questo è ortogonale a questa soluzione e, ad esempio, il blocco dei valori mutabili può essere facilmente utilizzato senza problemi. Devi solo esserne consapevole.

NB # 3 Se la struttura dei dati è enorme e la si modifica frequentemente, l'approccio copia su scrittura potrebbe essere proibitivo sia in termini di consumo di memoria che di costo della CPU per la copia. In tal caso, potresti voler utilizzare invece le raccolte immutabili di MS .


3

System.Collections.Generic.List<t>è già thread-safe per più lettori. Cercare di rendere il thread sicuro per più autori non avrebbe senso. (Per motivi già menzionati da Henk e Stephen)


Non riesci a vedere uno scenario in cui potrei avere 5 thread da aggiungere a un elenco? In questo modo è possibile visualizzare l'elenco accumulare record anche prima che terminino tutti.
Alan,

9
@Alan: sarebbe ConcurrentQueue, ConcurrentStack o ConcurrentBag. Per dare un senso a un ConcurrentList è necessario fornire un caso d'uso in cui le classi disponibili non sono sufficienti. Non vedo perché dovrei desiderare un accesso indicizzato quando gli elementi degli indici possono cambiare casualmente attraverso la rimozione simultanea. E per una lettura "bloccata" puoi già scattare istantanee delle classi concorrenti esistenti e metterle in un elenco.
Zarat,

Hai ragione - Non voglio un accesso indicizzato. In genere utilizzo IList <T> come proxy per un IEnumerable al quale posso aggiungere nuovi elementi. Ecco da dove viene la domanda, davvero.
Alan,

@Alan: Quindi vuoi una coda, non un elenco.
Billy ONeal,

3
Credo che tu abbia torto. Detto: sicuro per più lettori non significa che non puoi scrivere contemporaneamente. Scrivere significherebbe anche eliminare e si otterrà un errore se si elimina durante l'iterazione in esso.
Eric Ouellet,

2

Alcune persone hanno oscurato alcuni punti merce (e alcuni dei miei pensieri):

  • Potrebbe sembrare folle per impossibile accedere casualmente (indicizzatore) ma a me sembra bene. Devi solo pensare che ci sono molti metodi su raccolte multi-thread che potrebbero fallire come Indexer ed Delete. È inoltre possibile definire un'azione di errore (fallback) per l'accessor di scrittura come "fail" o semplicemente "aggiungi alla fine".
  • Non è perché è una raccolta multithread che verrà sempre utilizzata in un contesto multithread. Oppure potrebbe essere utilizzato anche da un solo scrittore e un lettore.
  • Un altro modo per essere in grado di utilizzare l'indicizzatore in modo sicuro potrebbe essere quello di avvolgere le azioni in un blocco della raccolta usando la sua radice (se resa pubblica).
  • Per molte persone, rendere visibile un rootLock diventa una buona pratica. Non sono sicuro al 100% su questo punto perché se è nascosto rimuovi molta flessibilità all'utente. Dobbiamo sempre ricordare che la programmazione del multithread non è per nessuno. Non possiamo impedire ogni tipo di utilizzo errato.
  • Microsoft dovrà fare un po 'di lavoro e definire alcuni nuovi standard per introdurre il corretto utilizzo della raccolta multithread. Innanzitutto IEnumerator non dovrebbe avere moveNext ma dovrebbe avere GetNext che restituisca true o false e ottenga un parametro di tipo T (in questo modo l'iterazione non bloccherebbe più). Inoltre, Microsoft già utilizza "utilizza" internamente in foreach ma a volte utilizza direttamente IEnumerator senza racchiuderlo in "using" (un bug nella vista raccolta e probabilmente in più punti) - L'utilizzo di IEnumerator nel wrapping è una pratica consigliata da Microsoft. Questo bug rimuove un buon potenziale per un iteratore sicuro ... Iteratore che blocca la raccolta nel costruttore e si sblocca sul suo metodo Dispose - per un metodo foreach di blocco.

Questa non è una risposta. Questi sono solo commenti che non si adattano realmente a un luogo specifico.

... La mia conclusione, Microsoft deve apportare alcune profonde modifiche al "foreach" per rendere la raccolta MultiThreaded più facile da usare. Inoltre deve seguire le proprie regole di utilizzo di IEnumerator. Fino a quel momento, possiamo scrivere facilmente una MultiThreadList che utilizzerebbe un iteratore di blocco ma che non seguirà "IList". Dovrai invece definire la propria interfaccia "IListPersonnal" che potrebbe non riuscire su "insert", "remove" e accessor casuale (indicizzatore) senza eccezioni. Ma chi vorrà usarlo se non è standard?


Si potrebbe facilmente scrivere un ConcurrentOrderedBag<T>che includerebbe un'implementazione di sola lettura di IList<T>, ma offrirebbe anche un int Add(T value)metodo completamente thread-safe . Non vedo perché ForEachsarebbero necessari cambiamenti. Sebbene Microsoft non lo dica esplicitamente, la loro pratica suggerisce che è perfettamente accettabile IEnumerator<T>enumerare i contenuti della raccolta esistenti al momento della creazione; l'eccezione modificata dalla raccolta è richiesta solo se l'enumeratore non è in grado di garantire un funzionamento privo di errori.
Supercat,

Scorrendo una collezione MT, il modo in cui è design potrebbe portare, come hai detto, a un'eccezione ... Quale non conosco. Intrappoleresti tutte le eccezioni? Nel mio libro l'eccezione è un'eccezione e non dovrebbe verificarsi nella normale esecuzione del codice. Altrimenti, per evitare eccezioni, è necessario bloccare la raccolta o ottenere una copia (in modo sicuro, ad esempio il blocco) o implementare un meccanismo molto complesso nella raccolta per evitare che si verifichino eccezioni a causa della concorrenza. Il mio pensiero era che sarebbe stato bello aggiungere un IEnumeratorMT che avrebbe bloccato la raccolta mentre si verificava un per ogni e aggiungendo un codice correlato ...
Eric Ouellet,

L'altra cosa che potrebbe anche accadere è che quando ottieni un iteratore, puoi bloccare la raccolta e quando il tuo iteratore viene raccolto GC puoi sbloccare la raccolta. Secondo Microsfot, controllano già se anche IEnumerable è un IDisposable e, in tal caso, chiamano il GC alla fine di ForEach. Il problema principale è che usano anche IEnumerable altrove senza chiamare il GC, quindi non puoi fare affidamento su questo. Avere una nuova interfaccia MT chiara per il blocco dell'abilitazione di IEnumerable risolverebbe il problema, almeno una parte di esso. (Non impedirebbe alle persone di non chiamarlo).
Eric Ouellet,

È una pessima forma per un GetEnumeratormetodo pubblico lasciare una raccolta bloccata dopo il suo ritorno; tali progetti possono facilmente portare a deadlock. Non IEnumerable<T>fornisce alcuna indicazione se ci si può aspettare che un'enumerazione venga completata anche se una raccolta viene modificata; il meglio che uno può fare è scrivere i propri metodi in modo che lo facciano e avere metodi che accettano IEnumerable<T>documentano il fatto che sarà sicuro per i IEnumerable<T>thread solo se l' enumerazione supporta i thread sicuri.
supercat

Ciò che sarebbe stato più utile sarebbe stato se IEnumerable<T>avesse incluso un metodo "Snapshot" con il tipo restituito IEnumerable<T>. Collezioni immutabili potrebbero restituire se stesse; una raccolta limitata potrebbe, se non altro, copiare se stessa in un List<T>oe T[]invocarla GetEnumerator. Alcune raccolte illimitate potrebbero implementare Snapshote quelle che non potrebbero essere in grado di generare un'eccezione senza provare a riempire un elenco con i loro contenuti.
supercat

1

Nell'esecuzione sequenziale del codice le strutture di dati utilizzate sono diverse dal codice (ben scritto) che esegue contemporaneamente. Il motivo è che il codice sequenziale implica un ordine implicito. Il codice concorrente tuttavia non implica alcun ordine; meglio ancora implica la mancanza di un ordine definito!

A causa di ciò, le strutture di dati con ordine implicito (come Elenco) non sono molto utili per risolvere problemi simultanei. Un elenco implica ordine, ma non definisce chiaramente quale sia tale ordine. Per questo motivo l'ordine di esecuzione del codice che manipola l'elenco determinerà (in una certa misura) l'ordine implicito dell'elenco, che è in conflitto diretto con un'efficace soluzione concorrente.

Ricorda che la concorrenza è un problema di dati, non un problema di codice! Non è possibile implementare prima il codice (o riscrivere il codice sequenziale esistente) e ottenere una soluzione concorrente ben progettata. È necessario progettare prima le strutture di dati tenendo presente che l'ordinamento implicito non esiste in un sistema concorrente.


1

l'approccio Lockless Copy and Write funziona alla grande se non hai a che fare con troppi elementi. Ecco una lezione che ho scritto:

public class CopyAndWriteList<T>
{
    public static List<T> Clear(List<T> list)
    {
        var a = new List<T>(list);
        a.Clear();
        return a;
    }

    public static List<T> Add(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Add(item);
        return a;
    }

    public static List<T> RemoveAt(List<T> list, int index)
    {
        var a = new List<T>(list);
        a.RemoveAt(index);
        return a;
    }

    public static List<T> Remove(List<T> list, T item)
    {
        var a = new List<T>(list);
        a.Remove(item);
        return a;
    }

}

esempio di utilizzo: ordini_BUY = CopyAndWriteList.Clear (ordini_BUY);


invece di bloccare, crea una copia dell'elenco, modifica l'elenco e imposta il riferimento al nuovo elenco. Quindi qualsiasi altro thread che sta ripetendo non causerà alcun problema.
Rob The Quant

0

Ne ho implementato uno simile a quello di Brian . Il mio è diverso:

  • Gestisco direttamente l'array.
  • Non inserisco i blocchi all'interno del blocco try.
  • Uso yield returnper produrre un enumeratore.
  • Sostengo la ricorsione del blocco. Ciò consente di leggere dall'elenco durante l'iterazione.
  • Uso i blocchi di lettura aggiornabili ove possibile.
  • DoSynce GetSyncmetodi che consentono interazioni sequenziali che richiedono l'accesso esclusivo all'elenco.

Il codice :

public class ConcurrentList<T> : IList<T>, IDisposable
{
    private ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
    private int _count = 0;

    public int Count
    {
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _count;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    public int InternalArrayLength
    { 
        get
        { 
            _lock.EnterReadLock();
            try
            {           
                return _arr.Length;
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }
    }

    private T[] _arr;

    public ConcurrentList(int initialCapacity)
    {
        _arr = new T[initialCapacity];
    }

    public ConcurrentList():this(4)
    { }

    public ConcurrentList(IEnumerable<T> items)
    {
        _arr = items.ToArray();
        _count = _arr.Length;
    }

    public void Add(T item)
    {
        _lock.EnterWriteLock();
        try
        {       
            var newCount = _count + 1;          
            EnsureCapacity(newCount);           
            _arr[_count] = item;
            _count = newCount;                  
        }
        finally
        {
            _lock.ExitWriteLock();
        }       
    }

    public void AddRange(IEnumerable<T> items)
    {
        if (items == null)
            throw new ArgumentNullException("items");

        _lock.EnterWriteLock();

        try
        {           
            var arr = items as T[] ?? items.ToArray();          
            var newCount = _count + arr.Length;
            EnsureCapacity(newCount);           
            Array.Copy(arr, 0, _arr, _count, arr.Length);       
            _count = newCount;
        }
        finally
        {
            _lock.ExitWriteLock();          
        }
    }

    private void EnsureCapacity(int capacity)
    {   
        if (_arr.Length >= capacity)
            return;

        int doubled;
        checked
        {
            try
            {           
                doubled = _arr.Length * 2;
            }
            catch (OverflowException)
            {
                doubled = int.MaxValue;
            }
        }

        var newLength = Math.Max(doubled, capacity);            
        Array.Resize(ref _arr, newLength);
    }

    public bool Remove(T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {           
            var i = IndexOfInternal(item);

            if (i == -1)
                return false;

            _lock.EnterWriteLock();
            try
            {   
                RemoveAtInternal(i);
                return true;
            }
            finally
            {               
                _lock.ExitWriteLock();
            }
        }
        finally
        {           
            _lock.ExitUpgradeableReadLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        _lock.EnterReadLock();

        try
        {    
            for (int i = 0; i < _count; i++)
                // deadlocking potential mitigated by lock recursion enforcement
                yield return _arr[i]; 
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item);
        }
        finally
        {
            _lock.ExitReadLock();
        }
    }

    private int IndexOfInternal(T item)
    {
        return Array.FindIndex(_arr, 0, _count, x => x.Equals(item));
    }

    public void Insert(int index, T item)
    {
        _lock.EnterUpgradeableReadLock();

        try
        {                       
            if (index > _count)
                throw new ArgumentOutOfRangeException("index"); 

            _lock.EnterWriteLock();
            try
            {       
                var newCount = _count + 1;
                EnsureCapacity(newCount);

                // shift everything right by one, starting at index
                Array.Copy(_arr, index, _arr, index + 1, _count - index);

                // insert
                _arr[index] = item;     
                _count = newCount;
            }
            finally
            {           
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }


    }

    public void RemoveAt(int index)
    {   
        _lock.EnterUpgradeableReadLock();
        try
        {   
            if (index >= _count)
                throw new ArgumentOutOfRangeException("index");

            _lock.EnterWriteLock();
            try
            {           
                RemoveAtInternal(index);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }
        finally
        {
            _lock.ExitUpgradeableReadLock();            
        }
    }

    private void RemoveAtInternal(int index)
    {           
        Array.Copy(_arr, index + 1, _arr, index, _count - index-1);
        _count--;

        // release last element
        Array.Clear(_arr, _count, 1);
    }

    public void Clear()
    {
        _lock.EnterWriteLock();
        try
        {        
            Array.Clear(_arr, 0, _count);
            _count = 0;
        }
        finally
        {           
            _lock.ExitWriteLock();
        }   
    }

    public bool Contains(T item)
    {
        _lock.EnterReadLock();
        try
        {   
            return IndexOfInternal(item) != -1;
        }
        finally
        {           
            _lock.ExitReadLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {       
        _lock.EnterReadLock();
        try
        {           
            if(_count > array.Length - arrayIndex)
                throw new ArgumentException("Destination array was not long enough.");

            Array.Copy(_arr, 0, array, arrayIndex, _count);
        }
        finally
        {
            _lock.ExitReadLock();           
        }
    }

    public bool IsReadOnly
    {   
        get { return false; }
    }

    public T this[int index]
    {
        get
        {
            _lock.EnterReadLock();
            try
            {           
                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                return _arr[index]; 
            }
            finally
            {
                _lock.ExitReadLock();               
            }           
        }
        set
        {
            _lock.EnterUpgradeableReadLock();
            try
            {

                if (index >= _count)
                    throw new ArgumentOutOfRangeException("index");

                _lock.EnterWriteLock();
                try
                {                       
                    _arr[index] = value;
                }
                finally
                {
                    _lock.ExitWriteLock();              
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }

        }
    }

    public void DoSync(Action<ConcurrentList<T>> action)
    {
        GetSync(l =>
        {
            action(l);
            return 0;
        });
    }

    public TResult GetSync<TResult>(Func<ConcurrentList<T>,TResult> func)
    {
        _lock.EnterWriteLock();
        try
        {           
            return func(this);
        }
        finally
        {
            _lock.ExitWriteLock();
        }
    }

    public void Dispose()
    {   
        _lock.Dispose();
    }
}

Cosa succede se due thread entrano all'inizio del tryblocco Removeo nel setter dell'indicizzatore contemporaneamente?
James,

@James che non sembra possibile. Leggi le osservazioni su msdn.microsoft.com/en-us/library/… . Eseguendo questo codice, non inserirai mai quel blocco una seconda volta: gist.github.com/ronnieoverby/59b715c3676127a113c3
Ronnie Overby

@Ronny Overby: interessante. Detto questo, sospetto che ciò avrebbe prestazioni molto migliori se si rimuovesse UpgradableReadLock da tutte le funzioni in cui l'unica operazione eseguita nel tempo tra il blocco di lettura aggiornabile e il blocco di scrittura - il sovraccarico di prendere qualsiasi tipo di blocco è molto di più rispetto al controllo per vedere se il parametro non è compreso nell'intervallo che semplicemente eseguendo tale controllo all'interno del blocco di scrittura probabilmente funzionerebbe meglio.
James,

Anche questa classe non sembra molto utile, poiché le funzioni basate sull'offset (la maggior parte di esse) non possono mai essere utilizzate in modo sicuro a meno che non ci sia comunque uno schema di blocco esterno perché la raccolta potrebbe cambiare tra quando si decide dove posizionare o ottenere qualcosa da e quando lo si ottiene effettivamente.
James,

1
Volevo andare sul disco dicendo che riconosco che l'utilità della IListsemantica in scenari simultanei è limitata nel migliore dei casi. Ho scritto questo codice probabilmente prima di arrivare a quella realizzazione. La mia esperienza è la stessa dello scrittore della risposta accettata: ho provato con quello che sapevo sulla sincronizzazione e IList <T> e ho imparato qualcosa facendo questo.
Ronnie Overby,
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.