2010-02-13 23 views
5

Se ho qualcosa di simile (pseudocodice):C# Lista multithread operazioni

class A 
{ 
    List<SomeClass> list; 

    private void clearList() 
    { 
     list = new List<SomeClass>(); 
    } 

    private void addElement() 
    { 
     list.Add(new SomeClass(...)); 
    } 
} 

è possibile che io incorrere in problemi di multithreading (o qualsiasi tipo di comportamento imprevisto) quando entrambe le funzioni vengono eseguite in parallelo?

Il caso d'uso è un elenco di errori che possono essere cancellati in qualsiasi momento (semplicemente assegnando una nuova lista vuota).

EDIT: mie ipotesi sono

  • solo thread aggiunge elementi
  • elementi dimenticati sono a posto (cioè condizione di competizione tra compensazione e l'aggiunta di un nuovo elemento), a condizione che l'operazione di cancellazione riesce senza problemi
  • .NET 2,0

risposta

10

ci sono due possibilità per problemi qui:

  • Gli elementi appena aggiunti potrebbero finire per essere dimenticati immediatamente, perché si cancella e si crea un nuovo elenco. È un problema? Fondamentalmente, se vengono chiamati allo stesso tempo AddElement e , si ha una condizione di competizione: l'elemento finirà nel nuovo elenco o nel vecchio (dimenticato).
  • List<T> non è sicuro per la mutazione multi-threaded, quindi se due thread differenti chiamano AddElement allo stesso tempo, i risultati non sono garantiti

Dato che si sta accedendo una risorsa condivisa, io personalmente tieni premuto un lucchetto mentre lo accedi. Dovrai comunque considerare la possibilità di cancellare l'elenco immediatamente prima/dopo l'aggiunta di un elemento.

EDIT: Il mio commento sul fatto che sia bene se si sta aggiungendo solo da un filo era già un po 'dubbia, per due motivi:

  • E' possibile (credo!) Che si potrebbe finire per cercare di aggiungere a un List<T> che non era ancora stato completamente costruito. Non ne sono sicuro, e il modello di memoria .NET 2.0 (al contrario di quello delle specifiche ECMA) potrebbe essere abbastanza forte da evitarlo, ma è difficile dire.
  • È possibile che il thread di aggiunta non "veda" immediatamente la modifica alla variabile list e continui a aggiungerla all'elenco precedente. Infatti, senza alcuna sincronizzazione , si poteva vedere il vecchio valore per sempre

Quando si aggiunge "l'iterazione nella GUI" nella miscela diventa davvero difficile - perché non è possibile modificare l'elenco mentre siete iterazione. La soluzione più semplice per questo è probabilmente quello di fornire un metodo che restituisce un copia della lista, e l'interfaccia utente può tranquillamente un'iterazione su che:

class A 
{ 
    private List<SomeClass> list; 
    private readonly object listLock = new object(); 

    private void ClearList() 
    { 
     lock (listLock) 
     { 
      list = new List<SomeClass>(); 
     } 
    } 

    private void AddElement() 
    { 
     lock (listLock) 
     { 
      list.Add(new SomeClass(...)); 
     } 
    } 

    private List<SomeClass> CopyList() 
    { 
     lock (listLock) 
     { 
      return new List<SomeClass>(list); 
     } 
    } 

} 
+0

Solo un thread aggiunge elementi, quindi il secondo punto non è un problema per me. E io ero a conoscenza delle condizioni della gara, ma per me non è molto importante - ma non sto affatto definendo "Clear", invece sto creando una nuova lista. Sarebbe un problema chiamare 'Clear' e' Add' nello stesso momento (hai detto che 'List' non è thread-safe)? – AndiDog

+0

Mi spiace, intendevo AddElement e ClearList. La semplice creazione di una nuova lista dovrebbe essere a posto, e se stai aggiungendo solo da un singolo thread dovrebbe andare tutto bene. Non dimenticare che ci possono essere ulteriori complicazioni quando stai leggendo anche dalla lista - succederebbe solo nella stessa discussione che sta facendo l'Add? –

+0

No, un altro thread GUI dovrebbe essere in grado di scorrere su di esso. Questo sarà implementato in poche settimane. Questo può causare problemi se la GUI usa 'foreach (Element e in instanceOfC.list) {...}' - se assegno un nuovo 'List' durante questa iterazione, la GUI funzionerà ancora sulla vecchia lista, giusto? – AndiDog

2

Sì - è possibile ,. In effetti, se vengono sinceramente chiamati allo stesso tempo, è altamente probabile.

Inoltre, è anche probabile che causi problemi se si verificano contemporaneamente due chiamate a addElement.

Per questo tipo di multithreading, è necessaria una sorta di blocco reciprocamente esclusivo attorno all'elenco stesso, in modo che sia possibile chiamare una sola operazione nell'elenco sottostante alla volta.

Una strategia di chiusura approssimativa su questo potrebbe aiutare. Qualcosa del tipo:

class A 
{ 
    static object myLock = new object() 
    List<SomeClass> list; 

    private void clearList() 
    { 
     lock(myLock) 
     { 
      list = new List<SomeClass>(); 
     } 

    } 

    private void addElement() 
    { 
     lock(myLock) 
     { 
      list.Add(new SomeClass(...)); 
     } 
    } 
} 
1

Non è proprio una buona cosa creare una nuova lista quando si desidera cancellarla.

Suppongo che sia stato assegnato anche un elenco nel costruttore in modo da non essere eseguito in un'eccezione del puntatore nullo.

Se si cancella e gli elementi vengono aggiunti, possono essere aggiunti alla vecchia lista che presumo va bene? MA se due elementi vengono aggiunti contemporaneamente, è possibile incontrare problemi.

concettualizzazione della .Net 4 nuove collezioni per gestire compiti multithreading :)

AGGIUNTA: sguardo al System.Collections.Concurrent namespace se si utilizza Net 4. Vi si possono trovare: System.Collections.Concurrent.ConcurrentBag<T> e molti altre belle collezioni :)

Si dovrebbe anche notare che il blocco può ridurre significativamente le prestazioni se non si guarda fuori.

+0

Scusa se non ho menzionato che sto usando .NET 2.0 - quindi ci sono built-in collezioni thread-safe in .NET 2.0? O devo usare 'lock'? – AndiDog

+0

Hhm, invece di dichiarare il blocco ecc. Esplicito, creo la mia classe, ThreadSafeList, dove i metodi sono bloccati. Quindi non devi scrivere blocco più volte (e forse dimenticare). –

+0

Ho appena trovato questo: http://blogs.msdn.com/jaredpar/archive/2009/02/11/why-are-thread-safe-collections-so-hard.aspx ma non ho guardato il codice così controllalo prima di usarlo :) –

1

Se si utilizza un'istanza di questa classe in più thread, sì. ti imbatterai in problemi Tutte le raccolte nel framework .Net (versione 3.5 e precedenti) NON sono thread-safe. Specialmente quando inizi a cambiare la raccolta mentre un altro thread lo sta itterando.

Utilizzare il blocco e distribuire "copie" di raccolte in ambienti con multithreading oppure, se è possibile utilizzare .Net 4.0, utilizzare le nuove raccolte concorrenti.

0

E 'chiaro dalle modifiche alla tua domanda che non ti importa dei soliti colpevoli qui - non ci sono in realtà chiamate simultanee ai metodi dello stesso oggetto.

In sostanza si sta chiedendo se è giusto assegnare il riferimento alla propria lista mentre è in corso l'accesso da un thread parallelo.

Per quanto ne so può ancora causare problemi. Tutto dipende da come l'assegnazione di riferimento è implementata a livello hardware. Per essere più precisi se questa operazione è atomica o meno.

Penso che per quanto sia sottile, esiste ancora la possibilità, soprattutto in ambienti multiprocessore, che il processo venga corrotto perché è stato aggiornato solo parzialmente quando è stato effettuato l'accesso.

+0

Non credo che le differenze hardware/l'assegnazione di riferimento possano causare problemi qui. Per quanto comprendo le specifiche C# (Sezione 5.5: http://msdn.microsoft.com/en-us/library/aa691278%28VS.71%29.aspx), l'assegnazione di riferimento deve essere atomica. – AndiDog

2

Le raccolte in .NET (fino a 3,5) non sono thread-safe o non-blocking (esecuzione parallela). Dovresti implementare il tuo derivando da IList e usa un ReaderWriterLockSlim per eseguire ogni azione. Ad esempio, il tuo metodo Aggiungi dovrebbe essere il seguente:

public void Add(T item) 
    { 
     _readerWriterLockSlim.EnterWriteLock(); 
     try { _actualList.Add(item); } 
     finally { _readerWriterLockSlim.ExitWriteLock(); } 
    } 

È necessario essere a conoscenza di alcuni trucchi di concorrenza qui. Ad esempio, devi avere un GetEnumerator che restituisce una nuova istanza come IList; non la lista attuale.Altrimenti incontrerai dei problemi; che dovrebbe essere simile:

public IEnumerator<T> GetEnumerator() 
    { 
     List<T> localList; 

     _lock.EnterReadLock(); 
     try { localList= new List<T>(_actualList); } 
     finally { _lock.ExitReadLock(); } 

     foreach (T item in localList) yield return item; 
    } 

e:

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 
    { 
     return ((IEnumerable<T>)this).GetEnumerator(); 
    } 

Nota: Quando si implementa collezioni thread-safe o parallele (e in effetti ogni altra classe) non derivano dalla classe, MA INTERFACCIA! Perché ci saranno sempre problemi legati alla struttura interna di quella classe o alcuni metodi che non sono virtuali e devi nasconderli e così via. Se devi farlo, fallo con molta attenzione!