Immagino che l'ottimizzatore venga ingannato dalla mancanza di parola chiave "volatile" sulla isComplete
variabile.
Ovviamente non puoi aggiungerlo, perché è una variabile locale. E naturalmente, poiché è una variabile locale, non dovrebbe essere affatto necessaria, perché i locali sono tenuti in pila e sono naturalmente sempre "freschi".
Tuttavia , dopo la compilazione, non è più una variabile locale . Poiché si accede a un delegato anonimo, il codice viene suddiviso e viene tradotto in una classe helper e in un campo membro, qualcosa come:
public static void Main(string[] args)
{
TheHelper hlp = new TheHelper();
var t = new Thread(hlp.Body);
t.Start();
Thread.Sleep(500);
hlp.isComplete = true;
t.Join();
Console.WriteLine("complete!");
}
private class TheHelper
{
public bool isComplete = false;
public void Body()
{
int i = 0;
while (!isComplete) i += 0;
}
}
Posso immaginare ora che il compilatore / ottimizzatore JIT in un ambiente multithread, durante l'elaborazione della TheHelper
classe, possa effettivamente memorizzare nella cache il valore false
in qualche registro o stack frame all'inizio del Body()
metodo e non aggiornarlo mai fino al termine del metodo. Questo perché NON C'È GARANZIA che il thread & metodo NON finirà prima che "= true" venga eseguito, quindi se non c'è garanzia, perché non metterlo nella cache e ottenere il miglioramento delle prestazioni della lettura dell'oggetto heap una volta invece di leggerlo ogni volta iterazione.
Questo è esattamente il motivo per cui volatile
esiste la parola chiave .
Affinché questa classe helper sia corretta un po 'meglio 1) in ambienti multi-thread, dovrebbe avere:
public volatile bool isComplete = false;
ma, ovviamente, poiché è codice generato automaticamente, non è possibile aggiungerlo. Un approccio migliore sarebbe quello di aggiungere alcuni messaggi di lock()
lettura e scrittura isCompleted
, o di utilizzare altre utilità di sincronizzazione o threading / tasking pronte per l'uso invece di provare a farlo bare-metal (che non sarà bare-metal, poiché è C # su CLR con GC, JIT e (..)).
La differenza nella modalità di debug si verifica probabilmente perché in modalità di debug sono escluse molte ottimizzazioni, quindi puoi, beh, eseguire il debug del codice che vedi sullo schermo. Pertanto while (!isComplete)
non è ottimizzato in modo da poter impostare un punto di interruzione lì, e quindi isComplete
non viene memorizzato nella cache in modo aggressivo in un registro o stack all'avvio del metodo e viene letto dall'oggetto sull'heap ad ogni iterazione del ciclo.
BTW. Sono solo le mie ipotesi su questo. Non ho nemmeno provato a compilarlo.
BTW. Non sembra essere un bug; è più simile a un effetto collaterale molto oscuro. Inoltre, se ho ragione, potrebbe trattarsi di una carenza di linguaggio: C # dovrebbe consentire di posizionare la parola chiave "volatile" sulle variabili locali che vengono acquisite e promosse a campi membro nelle chiusure.
1) vedi sotto per un commento di Eric Lippert su volatile
e / o questo articolo molto interessante che mostra i livelli di complessità coinvolti nell'assicurare che il codice che si basa volatile
sia sicuro ..uh, bene ..uh, diciamo OK.