Comportamento imprevisto per ThreadPool.QueueUserWorkItem

Si prega di controllare il codice di esempio qui sotto:

public class Sample { public int counter { get; set; } public string ID; public void RunCount() { for (int i = 0; i < counter; i++) { Thread.Sleep(1000); Console.WriteLine(this.ID + " : " + i.ToString()); } } } class Test { static void Main() { Sample[] arrSample = new Sample[4]; for (int i = 0; i  s.RunCount()); } Console.ReadKey(); } } 

L’output previsto per questo esempio dovrebbe essere qualcosa del tipo:

 Sample-0 : 0 Sample-1 : 0 Sample-2 : 0 Sample-3 : 0 Sample-0 : 1 Sample-1 : 1 Sample-2 : 1 Sample-3 : 1 . . . 

Tuttavia, quando si esegue questo codice, questo mostrerebbe qualcosa del genere:

 Sample-3 : 0 Sample-3 : 0 Sample-3 : 0 Sample-3 : 1 Sample-3 : 1 Sample-3 : 0 Sample-3 : 2 Sample-3 : 2 Sample-3 : 1 Sample-3 : 1 . . . 

Posso capire che l’ordine in cui i thread sono in esecuzione potrebbe differire e quindi il conteggio non aumenta in modo round robin. Tuttavia, non riesco a capire perché tutti gli ID siano visualizzati come Sample-3 , mentre l’esecuzione sta chiaramente accadendo indipendentemente l’una dall’altra.

Arent diversi oggetti utilizzati con thread diversi?

Questo è il vecchio problema di chiusura modificato. Si potrebbe voler guardare: Threadpools – ansible problema di ordine di esecuzione del thread per una domanda simile, e il post sul blog di Eric Lippert Closing over the loop variable considerato dannoso per la comprensione del problema.

Essenzialmente, l’espressione lambda che hai lì sta acquisendo la variabile s piuttosto che il valore della variabile nel punto in cui viene dichiarata la lambda. Di conseguenza, le successive modifiche apportate al valore della variabile sono visibili al delegato. L’istanza di Sample su cui verrà eseguito il metodo RunCount dipenderà RunCount a cui fa riferimento la variabile s (il suo valore) nel punto in cui il delegato viene effettivamente eseguito .

Inoltre, poiché i delegati (il compilatore in realtà riutilizza la stessa istanza delegato) vengono eseguiti in modo asincrono, non è garantito quali saranno questi valori nel punto di ogni esecuzione. Quello che state vedendo attualmente è che il ciclo foreach completato sul thread principale prima di una qualsiasi delle chiamate del delegato (ci si deve aspettare – ci vuole del tempo per pianificare le attività nel pool di thread). Quindi tutti gli elementi di lavoro finiscono per vedere il valore ‘finale’ della variabile del ciclo. Ma questo non è garantito in alcun modo; prova ad inserire un Thread.Sleep durata Thread.Sleep all’interno del ciclo e vedrai un output diverso.


La solita correzione è:

  1. Introdurre un’altra variabile all’interno del corpo del loop.
  2. Assegna quella variabile al valore corrente della variabile loop.
  3. Cattura la variabile ‘copia’ invece della variabile loop all’interno del lambda.

     foreach (Sample s in arrSample) { Sample sCopy = s; ThreadPool.QueueUserWorkItem(callback => sCopy.RunCount()); } 

Ora ogni object di lavoro “possiede” un particolare valore della variabile di loop.


Un’altra opzione in questo caso è di evitare completamente il problema non catturando nulla:

 ThreadPool.QueueUserWorkItem(obj => ((Sample)obj).RunCount(), s);