Un esempio riproducibile di utilizzo volatile

Sto cercando un esempio riproducibile che possa dimostrare come funziona la parola chiave volatile. Sto cercando qualcosa che funziona “male” senza una variabile (s) contrassegnata come volatile e funziona “correttamente” con essa.

Intendo un esempio che dimostrerà che l’ordine delle operazioni di scrittura / lettura durante l’esecuzione è diverso dal previsto quando la variabile non è contrassegnata come volatile e non è diversa quando la variabile non è contrassegnata come volatile.

Ho pensato di avere un esempio, ma poi con l’aiuto degli altri ho capito che era solo un pezzo di codice multithreading sbagliato. Perché volatile e MemoryBarrier non impediscono il riordino delle operazioni?

Ho anche trovato un collegamento che dimostra un effetto di volatile sull’ottimizzatore ma è diverso da quello che sto cercando. Dimostra che le richieste di variabili contrassegnate come volatili non verranno ottimizzate. Come illustrare l’utilizzo della parola chiave volatile in C #

Ecco dove sono arrivato così lontano. Questo codice non mostra alcun segno di riordino delle operazioni di lettura / scrittura. Ne sto cercando uno che mostrerà.

using System; using System.Threading; using System.Threading.Tasks; using System.Runtime.CompilerServices; namespace FlipFlop { class Program { //Declaring these variables static byte a; static byte b; //Track a number of iteration that it took to detect operation reordering. static long iterations = 0; static object locker = new object(); //Indicates that operation reordering is not found yet. static volatile bool continueTrying = true; //Indicates that Check method should continue. static volatile bool continueChecking = true; static void Main(string[] args) { //Restarting test until able to catch reordering. while (continueTrying) { iterations++; a = 0; b = 0; var checker = new Task(Check); var writter = new Task(Write); lock (locker) { continueChecking = true; checker.Start(); } writter.Start(); checker.Wait(); writter.Wait(); } Console.ReadKey(); } static void Write() { //Writing is locked until Main will start Check() method. lock (locker) { WriteInOneDirection(); WriteInOtherDirection(); //Stops spinning in the Check method. continueChecking = false; } } [MethodImpl(MethodImplOptions.NoInlining)] static void WriteInOneDirection(){ a = 1; b = 10; } [MethodImpl(MethodImplOptions.NoInlining)] static void WriteInOtherDirection() { b = 20; a = 2; } static void Check() { //Spins until finds operation reordering or stopped by Write method. while (continueChecking) { int tempA = a; int tempB = b; if (tempB == 10 && tempA == 2) { continueTrying = false; Console.WriteLine("Caught when a = {0} and b = {1}", tempA, tempB); Console.WriteLine("In " + iterations + " iterations."); break; } } } } } 

Modificare:

Come ho capito un’ottimizzazione che causa il riordino può venire da JITer o dall’hardware stesso. Posso riformulare la mia domanda. Le CPU JITer o x86 riordinano le operazioni di lettura / scrittura E c’è un modo per dimostrarlo in C # se lo fanno?

La semantica esatta di volatile è un dettaglio di implementazione del jitter. Il compilatore emette l’istruzione Opcodes.Volatile in cui si accede a una variabile dichiarata volatile. Fa alcuni controlli per verificare che il tipo di variabile sia legale, non puoi dichiarare tipi di valore più grandi di 4 byte volatili ma è lì che si ferma il dollaro.

Le specifiche del linguaggio C # definiscono il comportamento di volatile , citato qui da Eric Lippert. La semantica ‘release’ e ‘acquire’ è qualcosa che ha senso solo su un core del processore con un modello di memoria debole. Quei tipi di processori non hanno funzionato bene sul mercato, probabilmente perché sono un enorme dolore da programmare. Le probabilità che il tuo codice venga mai eseguito su un Titanium sono ridotte a zero.

Ciò che è particolarmente negativo nella definizione delle specifiche del linguaggio C # è che non menziona affatto ciò che realmente accade. La dichiarazione di una variabile volatile impedisce all’ottimizzatore del jitter di ottimizzare il codice per memorizzare la variabile in un registro cpu. Ecco perché il codice che Marc ha collegato è appeso. Questo avverrà solo con il jitter corrente di x86, un altro forte suggerimento che volatile è davvero un dettaglio di implementazione del jitter.

La povera semantica di volatile ha una ricca storia, viene dal linguaggio C. I cui generatori di codice hanno molti problemi a farlo bene. Ecco una relazione interessante su di esso (pdf) . Risale al 2008, una buona 30+ anni di opportunità per farlo bene. O sbagliato, questo va a pancia in giù quando il programma di ottimizzazione del codice dimentica che una variabile è volatile. Il codice non ottimizzato non ha mai un problema con esso. Notevole è che il jitter nella versione “open source” di .NET (SSLI20) ignora completamente l’istruzione IL. Si può anche sostenere che il comportamento corrente del jitter x86 sia un bug. Penso che lo sia, non è facile imbattersi nella modalità fallimento. Ma nessuno può obiettare che in realtà sia un bug.

La scrittura è sul muro, solo mai dichiarare una variabile volatile se è memorizzata in un registro mappato in memoria. L’intenzione originale della parola chiave. Le probabilità che ti imbatti in un tale uso nel linguaggio C # dovrebbero essere incredibilmente ridotte, codice come quello che appartiene a un driver di dispositivo. E soprattutto, non assumere mai che sia utile in uno scenario multi-thread.

È ansible utilizzare questo esempio per dimostrare il diverso comportamento con e senza volatile . Questo esempio deve essere compilato utilizzando una build Release ed eseguito all’esterno del debugger 1 . Prova aggiungendo e rimuovendo la parola chiave volatile sul flag stop .

Quello che succede qui è che la lettura di stop nel ciclo while viene riordinata in modo che si verifichi prima del ciclo se volatile è omesso. Ciò impedisce al thread di terminare anche dopo che il thread principale ha impostato il flag stop su true .

 class Program { static bool stop = false; public static void Main(string[] args) { var t = new Thread(() => { Console.WriteLine("thread begin"); bool toggle = false; while (!stop) { toggle = !toggle; } Console.WriteLine("thread end"); }); t.Start(); Thread.Sleep(1000); stop = true; Console.WriteLine("stop = true"); Console.WriteLine("waiting..."); // The Join call should return almost immediately. // With volatile it DOES. // Without volatile it does NOT. t.Join(); } } 

Va anche notato che sottili modifiche a questo esempio possono ridurre la sua probabilità di riproducibilità. Ad esempio, l’aggiunta di Thread.Sleep (forse per simulare l’interleaving del thread) introdurrà di per sé una barriera di memoria e quindi la semantica simile della parola chiave volatile . Sospetto che Console.WriteLine introduca barriere di memoria implicite o impedisca in altro modo al jitter di utilizzare l’operazione di riordino delle istruzioni. Tienilo a mente se inizi a scherzare troppo con l’esempio.


1 Credo che la versione del framework precedente alla 2.0 non includa questa ottimizzazione del riordino. Ciò significa che dovresti essere in grado di riprodurre questo comportamento con la versione 2.0 e successive, ma non con le versioni precedenti.