Ich habe einige Artikel über das volatile
Schlüsselwort gelesen , konnte aber die korrekte Verwendung nicht herausfinden. Könnten Sie mir bitte sagen, wofür es in C # und in Java verwendet werden soll?
Ich habe einige Artikel über das volatile
Schlüsselwort gelesen , konnte aber die korrekte Verwendung nicht herausfinden. Könnten Sie mir bitte sagen, wofür es in C # und in Java verwendet werden soll?
Antworten:
Sowohl für C # als auch für Java teilt "volatile" dem Compiler mit, dass der Wert einer Variablen niemals zwischengespeichert werden darf, da sich sein Wert außerhalb des Programmbereichs selbst ändern kann. Der Compiler vermeidet dann Optimierungen, die zu Problemen führen können, wenn sich die Variable "außerhalb ihrer Kontrolle" ändert.
Betrachten Sie dieses Beispiel:
int i = 5;
System.out.println(i);
Der Compiler kann dies so optimieren, dass nur 5 gedruckt werden:
System.out.println(5);
Wenn sich jedoch ein anderer Thread ändern kann i
, ist dies das falsche Verhalten. Wenn sich ein anderer Thread i
in 6 ändert , wird in der optimierten Version weiterhin 5 gedruckt.
Das volatile
Schlüsselwort verhindert eine solche Optimierung und Zwischenspeicherung und ist daher nützlich, wenn eine Variable von einem anderen Thread geändert werden kann.
i
markiert als volatile
. In Java dreht sich alles um Beziehungen, die vor dem Auftreten stattfinden.
i
es sich um eine lokale Variable handelt, kann sie ohnehin kein anderer Thread ändern. Wenn es sich um ein Feld handelt, kann der Compiler den Aufruf nur optimieren, wenn dies der Fall ist final
. Ich glaube nicht, dass der Compiler Optimierungen vornehmen kann, final
wenn er davon ausgeht, dass ein Feld "aussieht", wenn es nicht explizit als solches deklariert ist.
Um zu verstehen, was flüchtig mit einer Variablen macht, ist es wichtig zu verstehen, was passiert, wenn die Variable nicht flüchtig ist.
Wenn zwei Threads A & B auf eine nichtflüchtige Variable zugreifen, verwaltet jeder Thread eine lokale Kopie der Variablen in seinem lokalen Cache. Änderungen, die von Thread A im lokalen Cache vorgenommen wurden, sind für Thread B nicht sichtbar.
Wenn Variablen als flüchtig deklariert werden, bedeutet dies im Wesentlichen, dass Threads eine solche Variable nicht zwischenspeichern sollten, oder mit anderen Worten, Threads sollten den Werten dieser Variablen nicht vertrauen, es sei denn, sie werden direkt aus dem Hauptspeicher gelesen.
Wann sollte eine Variable volatil gemacht werden?
Wenn Sie eine Variable haben, auf die viele Threads zugreifen können, und Sie möchten, dass jeder Thread den neuesten aktualisierten Wert dieser Variablen erhält, auch wenn der Wert von einem anderen Thread / Prozess / außerhalb des Programms aktualisiert wird.
Lesevorgänge flüchtiger Felder haben Semantik erlangt . Dies bedeutet, dass garantiert ist, dass der aus der flüchtigen Variablen gelesene Speicher vor allen folgenden Speicherlesevorgängen auftritt. Es hindert den Compiler daran, die Neuordnung durchzuführen, und wenn die Hardware dies erfordert (schwach geordnete CPU), verwendet es eine spezielle Anweisung, um die Hardware dazu zu bringen, alle Lesevorgänge zu leeren, die nach dem flüchtigen Lesevorgang auftreten, aber spekulativ früh gestartet wurden, oder die CPU könnte Verhindern Sie, dass sie vorzeitig ausgegeben werden, indem Sie verhindern, dass zwischen der Ausgabe des Erwerbs der Last und ihrer Stilllegung spekulative Belastungen auftreten.
Schreibvorgänge flüchtiger Felder haben eine Release-Semantik . Dies bedeutet, dass garantiert wird, dass alle Speicherschreibvorgänge in die flüchtige Variable verzögert werden, bis alle vorherigen Speicherschreibvorgänge für andere Prozessoren sichtbar sind.
Betrachten Sie das folgende Beispiel:
something.foo = new Thing();
Wenn foo
es sich um eine Mitgliedsvariable in einer Klasse handelt und andere CPUs Zugriff auf die Objektinstanz haben, auf die verwiesen wird something
, wird möglicherweise die Wertänderung foo
angezeigt, bevor die Speicherschreibvorgänge im Thing
Konstruktor global sichtbar sind! Dies ist, was "schwach geordneter Speicher" bedeutet. Dies kann auch dann auftreten, wenn der Compiler alle Speicher im Konstruktor vor dem Speicher hat foo
. Wenn dies der Fall foo
ist, verfügt volatile
der Speicher über foo
eine Release-Semantik, und die Hardware garantiert, dass alle Schreibvorgänge vor dem Schreibvorgang foo
für andere Prozessoren sichtbar sind, bevor der Schreibvorgang ausgeführt werden kann foo
.
Wie ist es möglich, dass die Schreibvorgänge foo
so schlecht nachbestellt werden? Wenn sich die Cachezeile foo
im Cache befindet und die Speicher im Konstruktor den Cache verpasst haben, kann der Speicher möglicherweise viel früher abgeschlossen werden, als die Schreibvorgänge in den Cache fehlschlagen.
Die (schreckliche) Itanium-Architektur von Intel hatte Speicher schwach geordnet. Der in der ursprünglichen XBox 360 verwendete Prozessor hatte einen schwach geordneten Speicher. Viele ARM-Prozessoren, einschließlich des sehr beliebten ARMv7-A, haben einen schwach geordneten Speicher.
Entwickler sehen diese Datenrennen oft nicht, weil Dinge wie Sperren eine vollständige Speicherbarriere bilden, im Wesentlichen dasselbe wie das gleichzeitige Erfassen und Freigeben von Semantik. Es können keine Lasten innerhalb des Schlosses spekulativ ausgeführt werden, bevor das Schloss erworben wird. Sie werden verzögert, bis das Schloss erfasst wird. Während einer Sperrefreigabe können keine Speicher verzögert werden. Die Anweisung, die die Sperre aufhebt, wird verzögert, bis alle in der Sperre vorgenommenen Schreibvorgänge global sichtbar sind.
Ein vollständigeres Beispiel ist das Muster "Double-Checked Locking". Der Zweck dieses Musters besteht darin, zu vermeiden, dass immer eine Sperre erworben werden muss, um ein Objekt verzögert zu initialisieren.
Aus Wikipedia entnommen:
public class MySingleton {
private static object myLock = new object();
private static volatile MySingleton mySingleton = null;
private MySingleton() {
}
public static MySingleton GetInstance() {
if (mySingleton == null) { // 1st check
lock (myLock) {
if (mySingleton == null) { // 2nd (double) check
mySingleton = new MySingleton();
// Write-release semantics are implicitly handled by marking
// mySingleton with 'volatile', which inserts the necessary memory
// barriers between the constructor call and the write to mySingleton.
// The barriers created by the lock are not sufficient because
// the object is made visible before the lock is released.
}
}
}
// The barriers created by the lock are not sufficient because not all threads
// will acquire the lock. A fence for read-acquire semantics is needed between
// the test of mySingleton (above) and the use of its contents. This fence
// is automatically inserted because mySingleton is marked as 'volatile'.
return mySingleton;
}
}
In diesem Beispiel sind die Speicher im MySingleton
Konstruktor möglicherweise für andere Prozessoren vor dem Speicher nicht sichtbar mySingleton
. In diesem Fall erhalten die anderen Threads, die einen Blick auf mySingleton werfen, keine Sperre und nehmen die Schreibvorgänge an den Konstruktor nicht unbedingt auf.
volatile
verhindert niemals das Caching. Es garantiert die Reihenfolge, in der andere Prozessoren "sehen". Eine Speicherfreigabe verzögert einen Speicher, bis alle ausstehenden Schreibvorgänge abgeschlossen sind und ein Buszyklus ausgegeben wurde, der andere Prozessoren auffordert, ihre Cache-Zeile zu verwerfen / zurückzuschreiben, wenn sie zufällig die relevanten Zeilen zwischengespeichert haben. Bei einer Lasterfassung werden alle spekulierten Lesevorgänge gelöscht, um sicherzustellen, dass es sich nicht um veraltete Werte aus der Vergangenheit handelt.
head
und tail
müssen volatil sein, um zu verhindern, dass der Hersteller davon ausgeht, tail
dass sich nichts ändert, und um zu verhindern, dass der Verbraucher davon ausgeht, head
dass sich nichts ändert. Auch head
muss flüchtig sein , um sicherzustellen , dass die Warteschlangendaten schreiben sind global sichtbar , bevor das Geschäft zu head
global sichtbar ist.
Das flüchtige Schlüsselwort hat sowohl in Java als auch in C # unterschiedliche Bedeutungen.
Aus der Java-Sprachspezifikation :
Ein Feld kann als flüchtig deklariert werden. In diesem Fall stellt das Java-Speichermodell sicher, dass alle Threads einen konsistenten Wert für die Variable sehen.
Aus der C # -Referenz zum flüchtigen Schlüsselwort :
Das flüchtige Schlüsselwort gibt an, dass ein Feld im Programm durch etwas wie das Betriebssystem, die Hardware oder einen gleichzeitig ausgeführten Thread geändert werden kann.
In Java wird "flüchtig" verwendet, um der JVM mitzuteilen, dass die Variable von mehreren Threads gleichzeitig verwendet werden kann, sodass bestimmte allgemeine Optimierungen nicht angewendet werden können.
Insbesondere die Situation, in der die beiden Threads, die auf dieselbe Variable zugreifen, auf separaten CPUs auf demselben Computer ausgeführt werden. Es ist sehr üblich, dass CPUs die darin enthaltenen Daten aggressiv zwischenspeichern, da der Speicherzugriff sehr viel langsamer ist als der Cache-Zugriff. Dies bedeutet, dass die Daten, wenn sie in CPU1 aktualisiert werden, sofort alle Caches und den Hauptspeicher durchlaufen müssen, anstatt wenn der Cache sich selbst löscht, damit CPU2 den aktualisierten Wert sehen kann (wiederum durch Ignorieren aller Caches auf dem Weg).
Wenn Sie nichtflüchtige Daten lesen, erhält der ausführende Thread möglicherweise nicht immer den aktualisierten Wert. Wenn das Objekt jedoch flüchtig ist, erhält der Thread immer den aktuellsten Wert.
Volatile löst das Problem der Parallelität. Um diesen Wert synchron zu machen. Dieses Schlüsselwort wird hauptsächlich in einem Threading verwendet. Wenn mehrere Threads dieselbe Variable aktualisieren.