Un thread C # può realmente memorizzare un valore in cache e ignorare le modifiche a quel valore su altri thread?

Questa domanda NON riguarda le condizioni di gara, l’atomicità o il motivo per cui dovresti usare i blocchi nel tuo codice. So già di quelli.

AGGIORNAMENTO: La mia domanda non è “la stranezza con la memoria volatile esiste” (lo so che lo fa), la mia domanda è “il runtime .NET non è astratto così non lo vedrai mai”.

Vedi http://www.yoda.arachsys.com/csharp/threads/volatility.shtml e la prima risposta su È una proprietà stringa stessa threadsafe?

(Sono davvero lo stesso articolo dal momento che si fa riferimento all’altro.) Un thread imposta un bool e gli altri thread loop leggono per sempre quel bool – quegli articoli sostengono che il thread di lettura potrebbe memorizzare nella cache il vecchio valore e non leggere mai il nuovo valore, quindi quindi è necessario un blocco (o utilizzare la parola chiave volatile). Sostengono che il codice seguente sarà potenzialmente in loop per sempre. Ora sono d’accordo che è buona norma bloccare le variabili, ma non posso credere che il runtime .NET ignorerebbe davvero un valore della memoria che cambia come afferma l’articolo. Capisco i loro discorsi sulla memoria volatile rispetto alla memoria non volatile e sono d’accordo sul fatto che abbiano un punto valido nel codice non gestito , ma non posso credere che il runtime .NET non lo riassumerà correttamente in modo tale che il seguente codice cosa ti aspetti L’articolo ammette anche che il codice “quasi certamente” funzionerà (anche se non è garantito), quindi chiamerò BS sulla richiesta. Qualcuno può verificare che sia vero che il codice seguente non funzionerà sempre? Qualcuno è in grado di ottenere anche solo un caso (forse non è ansible riprodurlo sempre) dove questo fallisce?

class BackgroundTaskDemo { private bool stopping = false; static void Main() { BackgroundTaskDemo demo = new BackgroundTaskDemo(); new Thread(demo.DoWork).Start(); Thread.Sleep(5000); demo.stopping = true; } static void DoWork() { while (!stopping) { // Do something here } } } 

Il punto è: potrebbe funzionare, ma non è garantito il funzionamento da parte delle specifiche. Quello che le persone di solito cercano è un codice che funzioni per i giusti motivi, piuttosto che lavorare con una combinazione fluke del compilatore, del runtime e del JIT, che potrebbe cambiare tra le versioni del framework, la CPU fisica, la piattaforma e cose come x86 vs x64.

Comprendere il modello di memoria è un’area molto complessa e non pretendo di essere un esperto; ma le persone che sono dei veri esperti in quest’area mi assicurano che il comportamento che stai vedendo non è garantito.

Puoi pubblicare tutti gli esempi di lavoro che vuoi, ma sfortunatamente ciò non dimostra altro che “di solito funziona”. Certamente non dimostra che è garantito che funzioni. Basterebbe un solo contro-esempio per smentire, ma trovarlo è il problema …

No, non ne ho uno a portata di mano.


Aggiornamento con contro-esempio ripetibile:

 using System.Threading; using System; static class BackgroundTaskDemo { // make this volatile to fix it private static bool stopping = false; static void Main() { new Thread(DoWork).Start(); Thread.Sleep(5000); stopping = true; Console.WriteLine("Main exit"); Console.ReadLine(); } static void DoWork() { int i = 0; while (!stopping) { i++; } Console.WriteLine("DoWork exit " + i); } } 

Produzione:

 Main exit 

ma ancora in esecuzione, a pieno carico della CPU; nota che l’ stopping è stato impostato su true a questo punto. ReadLine è così che il processo non termina. L’ottimizzazione sembra dipendere dalla dimensione del codice all’interno del ciclo (da qui i++ ). Funziona ovviamente solo in modalità “rilascio”. Aggiungi volatile e tutto funziona bene.

Questo esempio include il codice x86 nativo come commenti per dimostrare che la variabile di controllo (‘stopLooping’) è memorizzata nella cache.

Cambia “stopLooping” in volatile per “correggerlo”.

Questo è stato creato con Visual Studio 2008 come versione build ed eseguito senza debug.

  using System; using System.Threading; /* A simple console application which demonstrates the need for the volatile keyword and shows the native x86 (JITed) code.*/ static class LoopForeverIfWeLoopOnce { private static bool stopLooping = false; static void Main() { new Thread(Loop).Start(); Thread.Sleep(1000); stopLooping = true; Console.Write("Main() is waiting for Enter to be pressed..."); Console.ReadLine(); Console.WriteLine("Main() is returning."); } static void Loop() { /* * Stack frame setup (Native x86 code): * 00000000 push ebp * 00000001 mov ebp,esp * 00000003 push edi * 00000004 push esi */ int i = 0; /* * Initialize 'i' to zero ('i' is in register edi) * 00000005 xor edi,edi */ while (!stopLooping) /* * Load 'stopLooping' into eax, test and skip loop if != 0 * 00000007 movzx eax,byte ptr ds:[001E2FE0h] * 0000000e test eax,eax * 00000010 jne 00000017 */ { i++; /* * Increment 'i' * 00000012 inc edi */ /* * Test the cached value of 'stopped' still in * register eax and do it again if it's still * zero (false), which it is if we get here: * 00000013 test eax,eax * 00000015 je 00000012 */ } Console.WriteLine("i={0}", i); } } 

FWIW:

  • Ho visto questa ottimizzazione del compilatore dal compilatore MS C ++ (codice non gestito).
  • Non so se succede in C #
  • Non succederà durante il debug (le ottimizzazioni del compilatore vengono disabilitate automaticamente durante il debug)
  • Anche se quell’ottimizzazione non avviene ora, stai scommettendo che non introdurranno mai quell’ottimizzazione nelle versioni future del compilatore JIT.