Veranschaulichung der Verwendung des flüchtigen Schlüsselworts in C #

88

Ich möchte ein kleines Programm codieren, das das Verhalten des volatileSchlüsselworts visuell veranschaulicht . Idealerweise sollte es sich um ein Programm handeln, das gleichzeitig auf ein nichtflüchtiges statisches Feld zugreift und aus diesem Grund ein falsches Verhalten aufweist.

Das Hinzufügen des flüchtigen Schlüsselworts im selben Programm sollte das Problem beheben.

Das habe ich nicht geschafft. Selbst wenn ich es mehrmals versuche, die Optimierung aktiviere usw., erhalte ich immer ein korrektes Verhalten ohne das Schlüsselwort "flüchtig".

Haben Sie eine Idee zu diesem Thema? Wissen Sie, wie Sie ein solches Problem in einer einfachen Demo-App simulieren können? Kommt es auf die Hardware an?

Romain Verdier
quelle

Antworten:

102

Ich habe ein funktionierendes Beispiel erreicht!

Die Hauptidee aus dem Wiki erhalten, aber mit einigen Änderungen für C #. Der Wiki-Artikel demonstriert dies für das statische Feld von C ++. Es sieht so aus, als würde C # Anforderungen an statische Felder immer sorgfältig kompilieren ... und ich mache ein Beispiel mit einem nicht statischen:

Wenn Sie dieses Beispiel im Release- Modus und ohne Debugger ausführen (dh mit Strg + F5), wird die Zeile while (test.foo != 255)auf 'while (true)' optimiert und dieses Programm kehrt nie zurück. Nach dem Hinzufügen eines volatileSchlüsselworts erhalten Sie jedoch immer "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
quelle
5
In .NET 4.0 getestet, können sowohl x86 als auch x64 bestätigen, dass das Beispiel weiterhin anwendbar ist. Vielen Dank! :)
Roman Starkov
1
Das ist nett! Ich wusste nie, dass die JIT diese Art der Optimierung durchführt und dass Volatile sie deaktiviert.
usr
3
Um genau zu sein, fügen Sie der Erklärung von foo das Schlüsselwort "volatile" hinzu, oder?
JoeCool
21

Ja, es ist hardwareabhängig (es ist unwahrscheinlich, dass das Problem ohne mehrere Prozessoren auftritt), aber es ist auch implementierungsabhängig. Die Speichermodellspezifikationen in der CLR-Spezifikation erlauben Dinge, die die Microsoft-Implementierung der CLR nicht unbedingt tut.

Craig Stuntz
quelle
6

Es handelt sich nicht wirklich um einen Fehler, der auftritt, wenn das Schlüsselwort "flüchtig" nicht angegeben ist, sondern vielmehr darum, dass ein Fehler auftreten kann, wenn er nicht angegeben wurde. Im Allgemeinen werden Sie wissen, wann dies der Fall ist, besser als der Compiler!

Die einfachste Art, darüber nachzudenken, wäre, dass der Compiler, wenn er wollte, bestimmte Werte inline setzen könnte. Indem Sie den Wert als flüchtig markieren, sagen Sie sich und dem Compiler, dass sich der Wert tatsächlich ändern kann (auch wenn der Compiler dies nicht glaubt). Dies bedeutet, dass der Compiler keine Inline-Werte verwenden, keinen Cache behalten oder den Wert nicht frühzeitig lesen sollte (in einem Optimierungsversuch).

Dieses Verhalten ist nicht wirklich das gleiche Schlüsselwort wie in C ++.

MSDN hat eine kurze Beschreibung hier . Hier ist ein vielleicht ausführlicherer Beitrag zu den Themen Volatilität, Atomizität und Interlocking

Ray Hayes
quelle
4

In C # ist dies schwer zu demonstrieren, da der Code von einer virtuellen Maschine abstrahiert wird. Auf einer Implementierung dieser Maschine funktioniert er also ohne Flüchtigkeit, während er auf einer anderen möglicherweise fehlschlägt.

Die Wikipedia hat jedoch ein gutes Beispiel dafür, wie man es in C demonstriert.

Dasselbe könnte in C # passieren, wenn der JIT-Compiler entscheidet, dass sich der Wert der Variablen ohnehin nicht ändern kann, und somit Maschinencode erstellt, der ihn nicht einmal mehr überprüft. Wenn jetzt ein anderer Thread den Wert ändert, befindet sich Ihr erster Thread möglicherweise immer noch in der Schleife.

Ein weiteres Beispiel ist Busy Waiting.

Auch dies könnte auch mit C # passieren, aber es hängt stark von der virtuellen Maschine und dem JIT-Compiler ab (oder vom Interpreter, wenn es keine JIT gibt ... theoretisch denke ich, dass MS immer einen JIT-Compiler verwendet und auch Mono eine; aber Sie können es möglicherweise manuell deaktivieren).

Mecki
quelle
4

Hier ist mein Beitrag zum kollektiven Verständnis dieses Verhaltens ... Es ist nicht viel, nur eine Demonstration (basierend auf der xkip-Demo), die das Verhalten eines flüchtigen Verses gegenüber einem nichtflüchtigen (dh "normalen") int-Wert nebeneinander zeigt -side, im selben Programm ... das war es, wonach ich gesucht habe, als ich diesen Thread gefunden habe.

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

  }
}

Es gibt einige wirklich ausgezeichnete prägnante Erklärungen oben sowie einige großartige Referenzen. Vielen Dank an alle, die mir geholfen haben, meinen Kopf volatileherumzukriegen (zumindest genug, um zu wissen, dass ich mich nicht darauf verlassen kann, volatilewo mein erster Instinkt warlock ).

Prost und Danke für den ganzen Fisch. Keith.


PS: Ich wäre sehr an einer Demo der ursprünglichen Anfrage interessiert, die lautete: "Ich würde gerne sehen, dass sich ein statischer flüchtiger Int korrekt verhält, während sich ein statischer Int schlecht .

Ich habe diese Herausforderung versucht und bin gescheitert. (Eigentlich habe ich ziemlich schnell aufgegeben ;-). Bei allem, was ich mit statischen Vars versucht habe, verhalten sie sich "richtig", unabhängig davon, ob sie flüchtig sind oder nicht ... und ich würde gerne erklären, WARUM das der Fall ist, wenn es tatsächlich der Fall ist ... Ist es das? Der Compiler speichert die Werte statischer Variablen nicht in Registern zwischen (dh er speichert stattdessen einen Verweis auf diese Heap-Adresse zwischen).

Nein, dies ist keine neue Frage. Es ist ein Versuch, die Community auf die ursprüngliche Frage zurückzubringen.

Corlettk
quelle
2

Ich bin auf den folgenden Text von Joe Albahari gestoßen, der mir sehr geholfen hat.

Ich habe ein Beispiel aus dem obigen Text genommen, das ich ein wenig geändert habe, indem ich ein statisches flüchtiges Feld erstellt habe. Wenn Sie das volatileSchlüsselwort entfernen, wird das Programm auf unbestimmte Zeit blockiert. Führen Sie dieses Beispiel im Freigabemodus aus .

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.
quelle