2012-04-04 11 views
7

Sto scorrendo un elenco di elementi con foreach, in questo modo:modifica gli elenchi da un altro thread, mentre l'iterazione (C#)

foreach (Type name in aList) { 
    name.doSomething(); 
} 

Tuttavia, in un altro thread che io chiamo qualcosa come

aList.Remove(Element); 

Durante il runtime, ciò provoca un'eccezione InvalidOperationException: Collection è stata modificata; l'operazione di enumerazione potrebbe non essere eseguita. Qual è il modo migliore per gestirlo (lo farei essere piuttosto semplice anche a costo di prestazioni)?

Grazie!

risposta

7

Discussione A:

lock (aList) { 
    foreach (Type name in aList) { 
    name.doSomething(); 
    } 
} 

Discussione B:

lock (aList) { 
    aList.Remove(Element); 
} 

Questo naturalmente è davvero male per le prestazioni.

+0

Grazie, funziona come un fascino :) Speriamo che non sarà possibile ottenere qualsiasi problemi di prestazioni utilizzando quello .. –

9

Qual è il modo migliore per gestirlo (vorrei renderlo piuttosto semplice anche a costo di prestazioni)?

Fondamentalmente: non provare a modificare una raccolta senza thread da più thread senza bloccare. Il fatto che stai iterando è per lo più irrilevante qui - ti ha semplicemente aiutato a trovarlo più velocemente. Sarebbe stato insicuro che due thread chiamassero allo stesso tempo Remove allo stesso tempo.

O utilizzare una collezione di thread-safe, come ConcurrentBago assicurarsi che solo un thread fa nulla con la raccolta in un momento.

+0

non avevo pensato, ma ora che lei ha detto che ha un senso. Grazie, probabilmente lo userò più tardi, ma per il solo caso la serratura funziona bene. –

0

se si desidera solo per evitare l'eccezione utilizzare

foreach (Type name in aList.ToArray()) 
{ name.doSomething(); } 

essere consapevoli del doSomething() viene eseguito anche nel caso l'elemento è stato rimosso in altro thread

+1

Sfortunatamente 'ToArray' e' Remove' potrebbero correre eventualmente con un diverso tipo di eccezione. Comunque è un buon pensiero! Fare riferimento alla mia [risposta] (http://stackoverflow.com/a/10018399/158779) per vedere come farlo funzionare correttamente. –

+0

Oh ... e benvenuti allo stackoverflow :) –

0

non posso in particolare raccontare dalla domanda, ma (sembra) si sta eseguendo un'azione su ogni elemento e quindi rimuoverlo. Potresti voler dare un'occhiata a BlockingCollection<T>, che ha un metodo che chiama GetConsumingEnumerable() per vedere se è adatto a te. Ecco un piccolo esempio.

void SomeMethod() 
{ 
    BlockingCollection<int> col = new BlockingCollection<int>(); 

    Task.StartNew(() => { 

     for (int j = 0; j < 50; j++) 
     { 
      col.Add(j); 
     } 

     col.CompleteAdding(); 

    }); 

    foreach (var item in col.GetConsumingEnumerable()) 
    { 
     //item is removed from the collection here, do something 
     Console.WriteLine(item); 
    } 
} 
9

Metodo # 1:

Il metodo più semplice e meno efficiente è quello di creare una sezione critica per i lettori e scrittori.

// Writer 
lock (aList) 
{ 
    aList.Remove(item); 
} 

// Reader 
lock (aList) 
{ 
    foreach (T name in aList) 
    { 
    name.doSomething(); 
    } 
} 

Metodo # 2:

Questo è simile al metodo # 1, ma invece di tenere il blocco per tutta la durata del ciclo foreach si dovrebbe copiare la raccolta e poi iterare il copia.

// Writer 
lock (aList) 
{ 
    aList.Remove(item); 
} 

// Reader 
List<T> copy; 
lock (aList) 
{ 
    copy = new List<T>(aList); 
} 
foreach (T name in copy) 
{ 
    name.doSomething(); 
} 

Metodo # 3:

Tutto dipende dalla vostra situazione specifica, ma il modo in cui io di solito affrontare questo è quello di mantenere il riferimento principale per la raccolta immutabile. In questo modo non dovrai mai sincronizzare l'accesso dal lato del lettore. Il lato scrittore delle cose ha bisogno di un lock. Il lato lettore non ha bisogno di nulla, il che significa che i lettori rimangono altamente concomitanti. L'unica cosa che devi fare è contrassegnare il riferimento aList come volatile.

// Variable declaration 
object lockref = new object(); 
volatile List<T> aList = new List<T>(); 

// Writer 
lock (lockref) 
{ 
    var copy = new List<T>(aList); 
    copy.Remove(item); 
    aList = copy; 
} 

// Reader 
List<T> local = aList; 
foreach (T name in local) 
{ 
    name.doSomething(); 
} 
+0

Mi piace la varietà di soluzioni qui, ma nei primi due esempi si dovrebbe notare che 'aList' dovrebbe essere un oggetto interno della classe altrimenti potrebbe verificarsi un deadlock. –

+0

@DerekW: Sì, questa è una buona pratica e ben nota. In realtà è improbabile che causi effettivamente un deadlock perché gli scenari tipici di tali occorrenze (lock annidati, ecc.) Non sono in gioco qui. Quindi, a meno che un altro pezzo di codice non abusi del tutto "a lista", il peggio che potrebbe accadere è un aumento della contesa della serratura. Penso che l'intero dibattito su "lock (this)" sia un po 'esagerato e si spinga un po' troppo verso il lato della paranoia di quanto alcuni ammetteranno. Ancora una volta, è comunque buona pratica evitare tali idiomi. –

+0

Vorrei anche sottolineare mentre ci sto pensando che # 3 deve essere seguito * esattamente * come presentato. Qualsiasi deviazione (come decidere che l'assegnazione a 'local' può essere omessa) porterà probabilmente a guasti casuali e spettacolari. Potrebbe anche strappare un intero nello spazio-tempo per tutto quello che so. –

Problemi correlati