Zilustrowanie użycia słowa kluczowego volatile w C #

88

Chciałbym napisać mały program, który wizualnie ilustruje zachowanie volatilesłowa kluczowego. Idealnie, powinien to być program, który wykonuje równoczesny dostęp do nieulotnego pola statycznego i który z tego powodu uzyskuje nieprawidłowe zachowanie.

Dodanie słowa kluczowego volatile w tym samym programie powinno rozwiązać problem.

To coś, czego nie udało mi się osiągnąć. Nawet próbując kilka razy, włączając optymalizację itp., Zawsze otrzymuję poprawne zachowanie bez słowa kluczowego „volatile”.

Masz jakiś pomysł na ten temat? Czy wiesz, jak zasymulować taki problem w prostej aplikacji demonstracyjnej? Czy to zależy od sprzętu?

Romain Verdier
źródło

Odpowiedzi:

102

Osiągnąłem działający przykład!

Główny pomysł otrzymany z wiki, ale z pewnymi zmianami dla C #. Artykuł wiki demonstruje to dla statycznego pola C ++, wygląda na to, że C # zawsze starannie kompiluje żądania do pól statycznych ... i robię przykład z niestatycznym polem:

Jeśli uruchomisz ten przykład w trybie wydania i bez debuggera (tj. Używając Ctrl + F5), linia while (test.foo != 255)zostanie zoptymalizowana do 'while (true)' i ten program nigdy nie zwróci. Ale po dodaniu volatilesłowa kluczowego zawsze otrzymujesz „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");
    }
}
S.Serpooshan
źródło
5
Testowane na platformie .NET 4.0, zarówno x86, jak i x64 - mogą potwierdzić, że przykład jest nadal aktualny. Dzięki! :)
Roman Starkov
1
To miłe! Nigdy nie wiedziałem, że JIT wykonuje tego rodzaju optymalizację, a ta niestabilność ją wyłącza.
usr
3
Żeby było jasne, dodajesz słowo kluczowe „volatile” do deklaracji foo, prawda?
JoeCool,
21

Tak, zależy to od sprzętu (jest mało prawdopodobne, aby problem wystąpił bez wielu procesorów), ale jest również zależny od implementacji. Specyfikacje modelu pamięci w specyfikacji CLR pozwalają na rzeczy, których implementacja CLR firmy Microsoft niekoniecznie robi.

Craig Stuntz
źródło
6

Tak naprawdę nie chodzi o to, że wystąpi błąd, gdy słowo kluczowe „volatile” nie zostanie określone, a raczej o błąd może się zdarzyć, gdy nie zostanie ono określone. Generalnie będziesz wiedział, kiedy tak jest, lepiej niż kompilator!

Najłatwiej o tym pomyśleć, że kompilator mógłby, gdyby chciał, wstawić pewne wartości. Oznaczając wartość jako zmienną, wmawiasz sobie i kompilatorowi, że wartość może się zmienić (nawet jeśli kompilator tak nie uważa). Oznacza to, że kompilator nie powinien wprowadzać wartości w wierszu, przechowywać pamięci podręcznej ani odczytywać wartości wcześnie (w celu optymalizacji).

To zachowanie nie jest w rzeczywistości tym samym słowem kluczowym, co w C ++.

MSDN ma krótki opis tutaj . Oto być może bardziej dogłębny post na temat zmienności, atomowości i blokowania

Ray Hayes
źródło
4

Trudno to zademonstrować w C #, ponieważ kod jest wyabstrahowany przez maszynę wirtualną, więc na jednej implementacji tej maszyny działa poprawnie bez ulotności, podczas gdy może zawieść na innej.

Wikipedia ma jednak dobry przykład, jak zademonstrować to w C.

To samo może się zdarzyć w C #, jeśli kompilator JIT zdecyduje, że wartość zmiennej i tak nie może się zmienić, a tym samym tworzy kod maszynowy, który już jej nie sprawdza. Jeśli teraz inny wątek zmienia wartość, Twój pierwszy wątek może nadal zostać złapany w pętli.

Innym przykładem jest oczekiwanie zajęte.

Ponownie, może się to zdarzyć również w C #, ale w dużym stopniu zależy to od maszyny wirtualnej i kompilatora JIT (lub interpretera, jeśli nie ma JIT ... w teorii myślę, że MS zawsze używa kompilatora JIT, a także używa Mono jeden; ale możesz być w stanie wyłączyć go ręcznie).

Mecki
źródło
4

Oto mój wkład w zbiorowe zrozumienie tego zachowania ... To niewiele, tylko demonstracja (oparta na demo xkipa), która pokazuje zachowanie niestabilnych wersetów nieulotną (tj. „Normalną”) wartość int, obok -z boku, w tym samym programie ... czego szukałem, gdy znalazłem ten wątek.

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();
    }

  }
}

Powyżej znajduje się kilka naprawdę doskonałych zwięzłych wyjaśnień, a także kilka świetnych odniesień. Dziękuję wszystkim za pomoc w poruszaniu się po głowie volatile(przynajmniej na tyle, żeby wiedzieć, volatilegdzie był mój pierwszy instynkt lock).

Pozdrawiam i dziękuję za WSZYSTKIE ryby. Keith.


PS: Byłbym bardzo zainteresowany w demo pierwotnego wniosku, który brzmiał: „Chciałbym zobaczyć jak statyczny lotnych int poprawnie zachowuje się w miarę do statycznej int misbehaves.

Próbowałem i zawiodłem to wyzwanie. (Właściwie zrezygnowałem dość szybko ;-). We wszystkim, co wypróbowałem ze statycznymi zmiennymi, zachowują się „poprawnie” niezależnie od tego, czy są niestabilne, czy nie ... i chciałbym wyjaśnić, DLACZEGO tak jest, jeśli rzeczywiście tak jest ... kompilator nie buforuje wartości zmiennych statycznych w rejestrach (tj. zamiast tego buforuje odniesienie do tego adresu sterty)?

Nie, to nie jest nowe pytanie ... to próba skierowania społeczności z powrotem na pierwotne pytanie.

Corlettk
źródło
2

Natrafiłem na następujący tekst Joe Albahari, który bardzo mi pomógł.

Wziąłem przykład z powyższego tekstu, który trochę zmieniłem, tworząc statyczne zmienne pole. Po usunięciu volatilesłowa kluczowego program będzie blokował się na czas nieokreślony. Uruchom ten przykład w trybie wydania .

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
    }
}
Martijn B
źródło