it-swarm-eu.dev

Wann sollte das volatile Keyword in C # verwendet werden?

Kann jemand eine gute Erklärung für das volatile Keyword in C # geben? Welche Probleme löst es und welche nicht? In welchen Fällen erspart es mir das Sperren?

279
Doron Yaacoby

Ich glaube nicht, dass es eine bessere Person gibt, um dies zu beantworten als Eric Lippert (Betonung im Original): 

In C # bedeutet "flüchtig" nicht nur "stellen Sie sicher, dass der Compiler und der Jitter keine Codeumordnung durchführen oder Cache-Optimierungen für diese Variable registrieren.". Es bedeutet auch "den Prozessoren sagen, dass .__ tun soll, was auch immer sie tun müssen, um sicherzustellen, dass ich den neuesten Wert lese, auch wenn dies bedeutet, dass andere Prozessoren angehalten und .__ Caches ".

Eigentlich ist das letzte Stück eine Lüge. Die wahre Semantik volatiler Lesevorgänge und Schreiben sind wesentlich komplexer als ich hier skizziert habe; im fact Sie garantieren nicht wirklich, dass jeder Prozessor das stoppt führt aus und aktualisiert Caches zum/vom Hauptspeicher. Vielmehr stellen sie Schwächer garantiert, wie der Speicher vor und nach dem Lesen und .__ zugreift. Es kann beobachtet werden, dass Schreibvorgänge in Bezug aufeinander angeordnet werden . Bestimmte Vorgänge, z. B. Erstellen eines neuen Threads, Eingeben einer Sperre oder Verwenden Sie eine der Interlocked-Familie von Methoden, um stärkere. garantiert über die Einhaltung der Bestellung. Wenn Sie mehr Details wünschen, Lesen Sie die Abschnitte 3.10 und 10.5.3 der C # 4.0-Spezifikation.

Ehrlich gesagt, Ich entmutige dich, niemals ein unbeständiges Feld zu machen. Flüchtig Felder sind ein Zeichen dafür, dass Sie etwas verrücktes tun: Sie sind Es wurde versucht, denselben Wert in zwei verschiedenen Threads zu lesen und zu schreiben. ohne ein Schloss zu setzen. Sperren garantieren, dass der Speicher gelesen wird oder Wird das Schloss innerhalb des Schlosses modifiziert, ist es konsistent, Schlösser garantieren dass jeweils nur ein Thread auf einen bestimmten Speicherplatz zugreift, und so auf. Die Anzahl der Situationen, in denen eine Sperre zu langsam ist, ist sehr klein, und die Wahrscheinlichkeit, dass Sie den Code falsch finden werden weil Sie nicht verstehen, dass das genaue Speichermodell sehr groß ist. ICH Versuchen Sie nicht, einen Low-Lock-Code mit Ausnahme des trivialsten .__ zu schreiben. Verwendungen ineinandergreifender Operationen. Ich überlasse die Verwendung von "flüchtig" echte Experten.

Weitere Informationen finden Sie unter:

254
Ohad Schneider

Wenn Sie etwas mehr über das volatile Keyword erfahren möchten, sollten Sie folgendes Programm in Betracht ziehen (ich verwende DevStudio 2005):

#include <iostream>
void main()
{
  int j = 0;
  for (int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
  {
    j += i;
  }
  std::cout << j;
}

Unter Verwendung der standardmäßigen (Release) Compiler-Einstellungen erstellt der Compiler den folgenden Assembler (IA32):

void main()
{
00401000  Push        ecx  
  int j = 0;
00401001  xor         ecx,ecx 
  for (int i = 0 ; i < 100 ; ++i)
00401003  xor         eax,eax 
00401005  mov         edx,1 
0040100A  lea         ebx,[ebx] 
  {
    j += i;
00401010  add         ecx,eax 
00401012  add         eax,edx 
00401014  cmp         eax,64h 
00401017  jl          main+10h (401010h) 
  }
  for (volatile int i = 0 ; i < 100 ; ++i)
00401019  mov         dword ptr [esp],0 
00401020  mov         eax,dword ptr [esp] 
00401023  cmp         eax,64h 
00401026  jge         main+3Eh (40103Eh) 
00401028  jmp         main+30h (401030h) 
0040102A  lea         ebx,[ebx] 
  {
    j += i;
00401030  add         ecx,dword ptr [esp] 
00401033  add         dword ptr [esp],edx 
00401036  mov         eax,dword ptr [esp] 
00401039  cmp         eax,64h 
0040103C  jl          main+30h (401030h) 
  }
  std::cout << j;
0040103E  Push        ecx  
0040103F  mov         ecx,dword ptr [__imp_std::cout (40203Ch)] 
00401045  call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)] 
}
0040104B  xor         eax,eax 
0040104D  pop         ecx  
0040104E  ret              

Bei der Ausgabe hat der Compiler entschieden, das ecx-Register zu verwenden, um den Wert der Variablen j zu speichern. Für die nichtflüchtige Schleife (die erste) hat der Compiler dem eax-Register i zugewiesen. Ziemliech direkt. Es gibt jedoch ein paar interessante Bits - der Befehl lea ebx, [ebx] ist effektiv ein Multibyte-Nop-Befehl, so dass die Schleife zu einer 16-Byte-ausgerichteten Speicheradresse springt. Die andere ist die Verwendung von edx, um den Schleifenzähler zu erhöhen, anstatt einen inc-Befehl zu verwenden. Der Befehl add reg, reg weist im Vergleich zum inc reg-Befehl auf einigen IA32-Kernen eine geringere Latenz auf, hat jedoch niemals eine höhere Latenz. 

Nun zur Schleife mit dem flüchtigen Schleifenzähler. Der Zähler ist in [esp] gespeichert und das volatile-Schlüsselwort teilt dem Compiler mit, dass der Wert immer aus dem Speicher gelesen/in den Speicher geschrieben und niemals einem Register zugewiesen werden sollte. Der Compiler geht sogar so weit, dass er beim Aktualisieren des Zählerwerts nicht in drei verschiedenen Schritten (load eax, inc eax, save eax) lädt/inkrementiert/speichert, sondern den Speicher direkt in einer einzigen Anweisung ändert (a add mem reg). Die Art und Weise, wie der Code erstellt wurde, stellt sicher, dass der Wert des Schleifenzählers im Kontext eines einzelnen CPU-Kerns immer auf dem neuesten Stand ist. Keine Operation an den Daten kann zu Beschädigungen oder Datenverlust führen (daher das load/inc/store nicht verwenden, da sich der Wert während des inc ändern kann und somit im Store verloren geht). Da Interrupts nur bearbeitet werden können, wenn der aktuelle Befehl abgeschlossen ist, können die Daten selbst bei nicht ausgerichtetem Speicher niemals beschädigt werden.

Wenn Sie eine zweite CPU in das System einführen, kann das volatile Schlüsselwort nicht gleichzeitig vor einer Aktualisierung der Daten durch eine andere CPU schützen. Im obigen Beispiel müssten Sie die Daten nicht abgleichen, um eine mögliche Beschädigung zu erhalten. Das volatile-Schlüsselwort verhindert keine potenzielle Beschädigung, wenn die Daten nicht atomar behandelt werden können. Wenn der Schleifenzähler beispielsweise vom Typ long (64 Bit) ist, sind zwei 32-Bit-Operationen erforderlich, um den Wert in der Mitte zu aktualisieren wodurch ein Interrupt auftreten kann und die Daten ändern.

Daher ist das volatile-Schlüsselwort nur für ausgerichtete Daten geeignet, die kleiner oder gleich der Größe der systemeigenen Register sind, so dass Operationen immer atomar sind.

Das volatile-Schlüsselwort wurde für die Verwendung mit IO -Operationen konzipiert, bei denen sich IO ständig ändern würde, die Adresse jedoch konstant war, beispielsweise ein UART - Speichergerät, und der Compiler sollte dies nicht tun Verwenden Sie den ersten aus der Adresse gelesenen Wert erneut.

Wenn Sie mit großen Daten arbeiten oder über mehrere CPUs verfügen, benötigen Sie ein Schließsystem auf höherer Ebene (OS), um den Datenzugriff ordnungsgemäß zu steuern.

54
Skizz

Wenn Sie .NET 1.1 verwenden, ist das volatile-Schlüsselwort erforderlich, wenn Sie doppelt gesperrte Sperren ausführen. Warum? Aufgrund von .NET 2.0 konnte das folgende Szenario dazu führen, dass ein zweiter Thread auf ein Objekt mit dem Wert null zugreifen kann, das jedoch noch nicht vollständig erstellt wurde:

  1. Thread 1 fragt, ob eine Variable null ist . // if (this.foo == null)
  2. Thread 1 legt fest, dass die Variable null ist, und gibt eine Sperre ein. //. // Sperre (this.bar)
  3. Thread 1 fragt AGAIN erneut, ob die Variable null ist . // if (this.foo == null)
  4. Thread 1 bestimmt weiterhin, dass die Variable null ist, ruft also einen Konstruktor auf und ordnet den Wert der Variablen zu . // this.foo = new Foo ();

Vor .NET 2.0 konnte diesem.foo die neue Instanz von Foo zugewiesen werden, bevor der Konstruktor fertig ausgeführt wurde. In diesem Fall könnte ein zweiter Thread (während des Aufrufs von Thread 1 an Foos Konstruktor) hereinkommen und Folgendes erfahren:

  1. Thread 2 fragt, ob die Variable null ist. // if (this.foo == null)
  2. Thread 2 legt fest, dass die Variable NICHT NULL ist, und versucht daher, sie zu verwenden . // this.foo.MakeFoo ()

Vor .NET 2.0 konnten Sie this.foo als flüchtig deklarieren, um dieses Problem zu umgehen. Seit .NET 2.0 müssen Sie das volatile-Schlüsselwort nicht mehr verwenden, um doppelt gesperrte Sperren durchzuführen.

Wikipedia hat tatsächlich einen guten Artikel zu Double Checked Locking und greift dieses Thema kurz auf: http://en.wikipedia.org/wiki/Double-checked_locking

38
AndrewTek

Von MSDN : Der flüchtige Modifikator wird normalerweise für ein Feld verwendet, auf das mehrere Threads zugreifen, ohne die Sperranweisung zum serialisieren des Zugriffs zu verwenden. Durch die Verwendung des flüchtigen Modifikators wird sichergestellt, dass ein Thread den aktuellsten Wert abruft, der von einem anderen Thread geschrieben wird.

22
Dr. Bob

Manchmal optimiert der Compiler ein Feld und verwendet ein Register, um es zu speichern. Wenn Thread 1 in das Feld schreibt und ein anderer Thread darauf zugreift, da das Update in einem Register (und nicht im Speicher) gespeichert wurde, erhält der 2. Thread veraltete Daten.

Sie können sich das volatile Keyword so vorstellen, dass es dem Compiler sagt: "Ich möchte, dass Sie diesen Wert im Speicher ablegen". Dies garantiert, dass der 2. Thread den neuesten Wert abruft.

20
Benoit

Die CLR optimiert die Anweisungen gerne, sodass beim Zugriff auf ein Feld im Code möglicherweise nicht immer auf den aktuellen Wert des Felds zugegriffen wird (dies kann vom Stack usw. kommen). Durch Markieren eines Felds als volatile wird sichergestellt, dass der Befehl auf den aktuellen Wert des Felds zugreift. Dies ist nützlich, wenn der Wert (in einem nicht sperrenden Szenario) durch einen gleichzeitigen Thread in Ihrem Programm oder einen anderen im Betriebssystem ausgeführten Code geändert werden kann.

Sie verlieren offensichtlich etwas Optimierung, aber der Code wird dadurch einfacher.

13
Joseph Daigle

Der Compiler ändert manchmal die Reihenfolge der Anweisungen im Code, um ihn zu optimieren. Normalerweise ist dies in Single-Threaded-Umgebungen kein Problem, in Multi-Threaded-Umgebungen kann dies jedoch ein Problem sein. Siehe folgendes Beispiel:

 private static int _flag = 0;
 private static int _value = 0;

 var t1 = Task.Run(() =>
 {
     _value = 10; /* compiler could switch these lines */
     _flag = 5;
 });

 var t2 = Task.Run(() =>
 {
     if (_flag == 5)
     {
         Console.WriteLine("Value: {0}", _value);
     }
 });

Wenn Sie t1 und t2 ausführen, erwarten Sie keine Ausgabe oder "Value: 10" als Ergebnis. Es kann sein, dass der Compiler die Zeile innerhalb der Funktion t1 umschaltet. Wenn t2 dann ausgeführt wird, könnte _flag den Wert 5 haben, _value jedoch 0. Daher kann die erwartete Logik beschädigt werden. 

Um dies zu beheben, können Sie das volatile - Schlüsselwort verwenden, das Sie auf das Feld anwenden können. Diese Anweisung deaktiviert die Compiler-Optimierungen, sodass Sie die richtige Reihenfolge in Ihrem Code erzwingen können.

private static volatile int _flag = 0;

Sie sollten volatile nur dann verwenden, wenn Sie es wirklich brauchen, da bestimmte Compiler-Optimierungen deaktiviert werden, was die Leistung beeinträchtigt. Es wird auch nicht von allen .NET-Sprachen unterstützt (Visual Basic unterstützt es nicht) und behindert daher die Interoperabilität von Sprachen.

0
Aliaksei Maniuk

Zusammenfassend lässt sich sagen, dass die richtige Antwort auf die Frage lautet: Wenn Ihr Code in 2.0 oder später ausgeführt wird, wird das volatile Schlüsselwort fast nie benötigt und schadet mehr als sinnvoll, wenn es unnötig verwendet wird. I.E. Verwenden Sie es niemals. ABER in früheren Versionen der Laufzeitumgebung IS war es für die korrekte Überprüfung der Überprüfung statischer Felder erforderlich. Speziell statische Felder, deren Klasse über statischen Klasseninitialisierungscode verfügt.

0
Paul Easter