Illustrare l'utilizzo della parola chiave volatile in C #


88

Vorrei codificare un programmino che illustri visivamente il comportamento della volatileparola chiave. Idealmente, dovrebbe essere un programma che esegue l'accesso simultaneo a un campo statico non volatile e che ottiene un comportamento errato a causa di ciò.

L'aggiunta della parola chiave volatile nello stesso programma dovrebbe risolvere il problema.

Quel qualcosa che non sono riuscito a ottenere. Anche provando più volte, abilitando l'ottimizzazione, ecc., Ottengo sempre un comportamento corretto senza la parola chiave "volatile".

Hai qualche idea su questo argomento? Sai come simulare un problema del genere in una semplice app demo? Dipende dall'hardware?

Risposte:


102

Ho realizzato un esempio funzionante!

L'idea principale ricevuta da wiki, ma con alcune modifiche per C #. L'articolo wiki lo dimostra per il campo statico di C ++, sembra che C # compili sempre attentamente le richieste ai campi statici ... e faccio un esempio con uno non statico:

Se esegui questo esempio in modalità di rilascio e senza debugger (cioè usando Ctrl + F5), la riga while (test.foo != 255)sarà ottimizzata a 'while (true)' e questo programma non ritorna mai. Ma dopo aver aggiunto la volatileparola chiave, ottieni sempre "OK".

class Test
{
    /*volatile*/ int foo;

    static void Main()
    {
        var test = new Test();

        new Thread(delegate() { Thread.Sleep(500); test.foo = 255; }).Start();

        while (test.foo != 255) ;
        Console.WriteLine("OK");
    }
}

5
Testato su .NET 4.0, sia x86 che x64, può confermare che l'esempio è ancora applicabile. Grazie! :)
Roman Starkov

1
Questo è carino! Non ho mai saputo che il JIT fa questo tipo di ottimizzazione e che volatile lo disabilita.
usr

3
Giusto per essere esplicito, stai aggiungendo la parola chiave "volatile" alla dichiarazione di foo, giusto?
JoeCool

21

Sì, dipende dall'hardware (è improbabile che tu veda il problema senza più processori), ma dipende anche dall'implementazione. Le specifiche del modello di memoria nelle specifiche CLR consentono cose che l'implementazione Microsoft di CLR non fa necessariamente.


6

Non si tratta realmente di un errore che si verifica quando la parola chiave "volatile" non è specificata, più che potrebbe verificarsi un errore quando non è stata specificata. Generalmente saprai quando questo è il caso meglio del compilatore!

Il modo più semplice per pensarci sarebbe che il compilatore potrebbe, se lo desidera, incorporare determinati valori. Contrassegnando il valore come volatile, stai dicendo a te stesso e al compilatore che il valore potrebbe effettivamente cambiare (anche se il compilatore non la pensa così). Ciò significa che il compilatore non dovrebbe inserire valori in linea, mantenere la cache o leggere il valore in anticipo (nel tentativo di ottimizzare).

Questo comportamento non è realmente la stessa parola chiave di C ++.

MSDN ha una breve descrizione qui . Ecco un post forse più approfondito sui temi della volatilità, atomicità e interblocco


4

È difficile da dimostrare in C #, poiché il codice è astratto da una macchina virtuale, quindi su un'implementazione di questa macchina funziona correttamente senza volatile, mentre potrebbe non funzionare su un'altra.

Tuttavia, Wikipedia ha un buon esempio di come dimostrarlo in C.

La stessa cosa potrebbe accadere in C # se il compilatore JIT decide che il valore della variabile non può cambiare comunque e crea così un codice macchina che non lo controlla nemmeno più. Se ora un altro thread stava cambiando il valore, il tuo primo thread potrebbe ancora essere catturato nel ciclo.

Un altro esempio è Occupato in attesa.

Di nuovo, questo potrebbe accadere anche con C #, ma dipende fortemente dalla macchina virtuale e dal compilatore JIT (o interprete, se non ha JIT ... in teoria, penso che MS utilizzi sempre un compilatore JIT e anche Mono usa uno; ma potresti essere in grado di disabilitarlo manualmente).


4

Ecco il mio contributo alla comprensione collettiva di questo comportamento ... Non è molto, solo una dimostrazione (basata sulla demo di xkip) che mostra il comportamento di un volatile versi un valore int non volatile (cioè "normale"), fianco a fianco -side, nello stesso programma ... che è quello che stavo cercando quando ho trovato questo thread.

using System;
using System.Threading;

namespace VolatileTest
{
  class VolatileTest 
  {
    private volatile int _volatileInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _volatileInt = 1; }).Start();
      while ( _volatileInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_volatileInt="+_volatileInt);
    }
  }

  class NormalTest 
  {
    private int _normalInt;
    public void Run() {
      new Thread(delegate() { Thread.Sleep(500); _normalInt = 1; }).Start();
      // NOTE: Program hangs here in Release mode only (not Debug mode).
      // See: http://stackoverflow.com/questions/133270/illustrating-usage-of-the-volatile-keyword-in-c-sharp
      // for an explanation of why. The short answer is because the
      // compiler optimisation caches _normalInt on a register, so
      // it never re-reads the value of the _normalInt variable, so
      // it never sees the modified value. Ergo: while ( true )!!!!
      while ( _normalInt != 1 ) 
        ; // Do nothing
      Console.WriteLine("_normalInt="+_normalInt);
    }
  }

  class Program
  {
    static void Main() {
#if DEBUG
      Console.WriteLine("You must run this program in Release mode to reproduce the problem!");
#endif
      new VolatileTest().Run();
      Console.WriteLine("This program will now hang!");
      new NormalTest().Run();
    }

  }
}

Ci sono alcune spiegazioni succinte davvero eccellenti sopra, oltre ad alcuni ottimi riferimenti. Grazie a tutti per avermi aiutato a capire volatile(almeno abbastanza da sapere non fare affidamento su volatiledove fosse il mio primo istinto lock).

Saluti e grazie per TUTTO il pesce. Keith.


PS: Sarei molto interessato a una demo della richiesta originale, che era: "Mi piacerebbe vedere un int volatile statico che si comporta correttamente quando un int statico si comporta male.

Ho provato e fallito questa sfida. (In realtà ho rinunciato abbastanza velocemente ;-). In tutto ciò che ho provato con le variabili statiche si comportano "correttamente" indipendentemente dal fatto che siano o meno volatili ... e mi piacerebbe una spiegazione del PERCHE 'è così, se davvero è così ... È così il compilatore non memorizza nella cache i valori delle variabili statiche nei registri (cioè memorizza nella cache invece un riferimento a quell'indirizzo di heap)?

No questa non è una nuova domanda ... è un tentativo di Stear la comunità torna alla domanda iniziale.


2

Mi sono imbattuto nel seguente testo di Joe Albahari che mi ha aiutato molto.

Ho preso un esempio dal testo sopra che ho alterato un po ', creando un campo volatile statico. Quando rimuovi la volatileparola chiave, il programma si bloccherà indefinitamente. Esegui questo esempio in modalità di rilascio .

class Program
{
    public static volatile bool complete = false;

    private static void Main()
    {           
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!complete) toggle = !toggle;
        });

        t.Start();
        Thread.Sleep(1000); //let the other thread spin up
        complete = true;
        t.Join(); // Blocks indefinitely when you remove volatile
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.