.NET multithreading, volatile e modello di memoria

Supponiamo di avere il seguente codice:

class Program { static volatile bool flag1; static volatile bool flag2; static volatile int val; static void Main(string[] args) { for (int i = 0; i < 10000 * 10000; i++) { if (i % 500000 == 0) { Console.WriteLine("{0:#,0}",i); } flag1 = false; flag2 = false; val = 0; Parallel.Invoke(A1, A2); if (val == 0) throw new Exception(string.Format("{0:#,0}: {1}, {2}", i, flag1, flag2)); } } static void A1() { flag2 = true; if (flag1) val = 1; } static void A2() { flag1 = true; if (flag2) val = 2; } } } 

È colpa! La quastion principale è Perché … Suppongo che le operazioni di riordino della CPU con flag1 = true; e se dichiarazione (flag2), ma le variabili flag1 e flag2 sono contrassegnate come campi volatili …

Nel modello di memoria .NET, il runtime (CLI) assicurerà che le modifiche ai campi volatili non vengano memorizzate nella cache dei registri, quindi una modifica su qualsiasi thread viene immediatamente vista su altri thread ( NB questo non è vero in altri modelli di memoria, incluso Java ).

Ma questo non dice nulla sull’ordinamento relativo delle operazioni su campi multipli, volatili o meno.

Per fornire un ordinamento coerente su più campi, è necessario utilizzare un blocco (o una barriera di memoria, in modo esplicito o implicito con uno dei metodi che includono una barriera di memoria).

Per maggiori dettagli vedi “Concurrent Programming on Windows”, Joe Duffy, AW, 2008

La specifica ECMA-335 dice:

Una lettura volatile ha “acquisisce semantica” nel senso che la lettura è garantita per verificarsi prima di qualsiasi riferimento alla memoria che si verifica dopo l’istruzione di lettura nella sequenza di istruzioni CIL. Una scrittura volatile ha una “semantica di rilascio” che significa che la scrittura è garantita per accadere dopo qualsiasi riferimento di memoria prima dell’istruzione di scrittura nella sequenza di istruzioni CIL. Un’attuazione conforms della CLI deve garantire questa semantica delle operazioni volatili. Ciò garantisce che tutti i thread osservino le scritture volatili eseguite da qualsiasi altro thread nell’ordine in cui sono state eseguite. Ma un’implementazione conforms non è richiesta per fornire un singolo ordinamento totale di scritture volatili come visto da tutti i thread di esecuzione.

Disegniamo come appare:

inserisci la descrizione dell'immagine qui

Quindi, abbiamo due semi-recinti: uno per la scrittura volatile e uno per la lettura volatile. E non ci stanno proteggendo dal riordino delle istruzioni tra di loro.
Inoltre, anche su architetture così rigide come AMD64 (x86-64) è consentito riordinare i negozi dopo i carichi .
E per altre architetture con un modello di memoria hardware più debole è ansible osservare anche cose più divertenti. Su ARM è ansible ottenere oggetti parzialmente costruiti osservati se il riferimento è stato assegnato in modo non volatile.

Per correggere il tuo esempio dovresti semplicemente inserire le chiamate Thread.MemoryBarrier() tra assignment e if-clause:

 static void A1() { flag2 = true; Thread.MemoryBarrier(); if (flag1) val = 1; } static void A2() { flag1 = true; Thread.MemoryBarrier(); if (flag2) val = 2; } 

Questo ci proteggerà dal riordino di queste istruzioni aggiungendo il recinto completo.