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?
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:
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.
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:
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:
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
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.
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.
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.
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.
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.