flüchtig (Computerprogrammierung) - volatile (computer programming)
In Computer - Programmierung , vor allem in der C , C ++ , C # und Java Programmiersprachen , das volatile Schlüsselwort gibt an, dass ein Wert zwischen verschiedenen Zugriffen ändern kann, auch wenn es nicht geändert zu werden scheint. Dieses Schlüsselwort verhindert, dass ein optimierender Compiler nachfolgende Lese- oder Schreibvorgänge wegoptimiert und somit einen veralteten Wert falsch wiederverwendet oder Schreibvorgänge auslässt. Flüchtige Werte treten hauptsächlich beim Hardwarezugriff ( Memory-mapped I/O ) auf, bei dem das Lesen aus oder das Schreiben in den Speicher verwendet wird, um mit Peripheriegeräten zu kommunizieren , und beim Threading , bei dem ein anderer Thread einen Wert geändert haben kann.
Obwohl es sich um ein häufiges Schlüsselwort handelt, unterscheidet sich das Verhalten von volatileerheblich zwischen den Programmiersprachen und wird leicht missverstanden. In C und C++ ist es ein Typqualifizierer wie const, und ist eine Eigenschaft des Typs . Weiterhin ist in C und C ++ funktioniert es nicht funktioniert in den meisten Threading Szenarien, und dass die Verwendung abgeraten. In Java und C# ist es eine Eigenschaft einer Variablen und gibt an, dass das Objekt, an das die Variable gebunden ist, mutieren kann und speziell für das Threading gedacht ist. In der Programmiersprache D gibt es ein separates Schlüsselwort sharedfür die Threading-Verwendung, aber es volatileexistiert kein Schlüsselwort.
In C und C++
In C und folglich in C++ sollte das volatileSchlüsselwort
- erlauben den Zugriff auf speicherabgebildete E/A- Geräte
- erlauben die Verwendung von Variablen zwischen
setjmpundlongjmp - erlauben die Verwendung von
sig_atomic_tVariablen in Signalhandlern.
Obwohl sowohl von C als auch von C++ beabsichtigt, können die C-Standards nicht ausdrücken, dass sich die volatileSemantik auf den lvalue bezieht, nicht auf das referenzierte Objekt. Die entsprechende Mängelanzeige DR 476 (zu C11) wird noch mit C17 geprüft.
Operationen auf volatileVariablen sind weder atomar noch stellen sie eine richtige Vorher-Beziehung für das Threading her. Dies ist in den relevanten Standards (C, C++, POSIX , WIN32) spezifiziert, und volatile Variablen sind in den allermeisten aktuellen Implementierungen nicht threadsicher. Daher volatilewird von vielen C/C++-Gruppen von der Verwendung des Schlüsselworts als tragbarer Synchronisationsmechanismus abgeraten.
Beispiel für speicherabgebildete E/A in C
In diesem Beispiel setzt der Code den in gespeicherten Wert fooauf 0. Es beginnt dann, diesen Wert wiederholt abzufragen , bis er sich in ändert 255:
static int foo;
void bar(void) {
foo = 0;
while (foo != 255)
;
}
Ein optimierender Compiler wird feststellen, dass kein anderer Code den in gespeicherten Wert möglicherweise ändern kann foo, und geht davon aus, dass er 0jederzeit gleich bleibt . Der Compiler ersetzt daher den Funktionsrumpf durch eine Endlosschleife ähnlich dieser:
void bar_optimized(void) {
foo = 0;
while (true)
;
}
Jedoch fookönnte eine Stelle dar , die durch andere Elemente des Computersystems zu jeder Zeit, wie beispielsweise ein verändert werden können Hardwareregister einer Vorrichtung mit der CPU . Der obige Code würde eine solche Änderung niemals erkennen; ohne das volatileSchlüsselwort geht der Compiler davon aus, dass das aktuelle Programm der einzige Teil des Systems ist, der den Wert ändern könnte (was bei weitem die häufigste Situation ist).
Um zu verhindern, dass der Compiler Code wie oben beschrieben optimiert, wird das volatileSchlüsselwort verwendet:
static volatile int foo;
void bar (void) {
foo = 0;
while (foo != 255)
;
}
Bei dieser Modifikation wird die Schleifenbedingung nicht wegoptimiert und das System erkennt die Änderung, wenn sie auftritt.
Im Allgemeinen gibt es auf Plattformen verfügbare Speicherbarriereoperationen (die in C++11 verfügbar gemacht werden), die anstelle von flüchtigen bevorzugt werden sollten, da sie dem Compiler eine bessere Optimierung ermöglichen und vor allem ein korrektes Verhalten in Multithread-Szenarien garantieren. Weder die C-Spezifikation (vor C11) noch die C++-Spezifikation (vor C++11) spezifizieren ein Multithread-Speichermodell, sodass sich volatile möglicherweise nicht deterministisch über Betriebssysteme/Compiler/CPUs hinweg verhalten.
Optimierungsvergleich in C
Die folgenden C-Programme und zugehörigen Assemblys veranschaulichen, wie sich das volatileSchlüsselwort auf die Ausgabe des Compilers auswirkt. Der Compiler war in diesem Fall GCC .
Beim Betrachten des Assemblercodes ist deutlich zu erkennen, dass der mit volatileObjekten generierte Code ausführlicher ist, was ihn länger macht, damit die Natur von volatileObjekten erfüllt werden kann. Das volatileSchlüsselwort verhindert, dass der Compiler eine Optimierung des Codes mit flüchtigen Objekten durchführt, wodurch sichergestellt wird, dass jede flüchtige Variablenzuweisung und jeder Lesevorgang einen entsprechenden Speicherzugriff hat. Ohne das volatileSchlüsselwort weiß der Compiler, dass eine Variable nicht bei jeder Verwendung neu aus dem Speicher gelesen werden muss, da keine Schreibvorgänge von anderen Threads oder Prozessen in ihre Speicherposition erfolgen sollten.
| Baugruppenvergleich | |
|---|---|
Ohne volatileStichwort |
Mit volatileStichwort
|
# include <stdio.h>
int main() {
/* These variables will never be created on stack*/
int a = 10, b = 100, c = 0, d = 0;
/* "printf" will be called with arguments "%d" and
110 (the compiler computes the sum of a+b),
hence no overhead of performing addition at
run-time */
printf("%d", a + b);
/* This code will be removed via optimization, but
the impact of 'c' and 'd' becoming 100 can be
seen while calling "printf" */
a = b;
c = b;
d = b;
/* Compiler will generate code where printf is
called with arguments "%d" and 200 */
printf("%d", c + d);
return 0;
}
|
# include <stdio.h>
int main() {
volatile int a = 10, b = 100, c = 0, d = 0;
printf("%d", a + b);
a = b;
c = b;
d = b;
printf("%d", c + d);
return 0;
}
|
| gcc -S -O3 -masm=intel noVolatileVar.c -o ohne.s | gcc -S -O3 -masm=intel VolatileVar.c -o with.s |
.file "noVolatileVar.c"
.intel_syntax noprefix
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB11:
.cfi_startproc
sub rsp, 8
.cfi_def_cfa_offset 16
mov esi, 110
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
mov esi, 200
mov edi, OFFSET FLAT:.LC0
xor eax, eax
call printf
xor eax, eax
add rsp, 8
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (GNU) 4.8.2"
.section .note.GNU-stack,"",@progbits
|
.file "VolatileVar.c"
.intel_syntax noprefix
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d"
.section .text.startup,"ax",@progbits
.p2align 4,,15
.globl main
.type main, @function
main:
.LFB11:
.cfi_startproc
sub rsp, 24
.cfi_def_cfa_offset 32
mov edi, OFFSET FLAT:.LC0
mov DWORD PTR [rsp], 10
mov DWORD PTR [rsp+4], 100
mov DWORD PTR [rsp+8], 0
mov DWORD PTR [rsp+12], 0
mov esi, DWORD PTR [rsp]
mov eax, DWORD PTR [rsp+4]
add esi, eax
xor eax, eax
call printf
mov eax, DWORD PTR [rsp+4]
mov edi, OFFSET FLAT:.LC0
mov DWORD PTR [rsp], eax
mov eax, DWORD PTR [rsp+4]
mov DWORD PTR [rsp+8], eax
mov eax, DWORD PTR [rsp+4]
mov DWORD PTR [rsp+12], eax
mov esi, DWORD PTR [rsp+8]
mov eax, DWORD PTR [rsp+12]
add esi, eax
xor eax, eax
call printf
xor eax, eax
add rsp, 24
.cfi_def_cfa_offset 8
ret
.cfi_endproc
.LFE11:
.size main, .-main
.ident "GCC: (GNU) 4.8.2"
.section .note.GNU-stack,"",@progbits
|
C++11
Laut C++11 ISO-Standard ist das Schlüsselwort volatile nur für den Hardwarezugriff gedacht; Verwenden Sie es nicht für die Kommunikation zwischen Threads. Für die Kommunikation zwischen Threads stellt die Standardbibliothek std::atomic<T>Vorlagen bereit .
Auf Java
Die Programmiersprache Java hat auch das volatileSchlüsselwort, aber es wird für einen etwas anderen Zweck verwendet. Bei Anwendung auf ein Feld bietet der Java-Qualifizierer volatiledie folgenden Garantien:
- In allen Java-Versionen gibt es eine globale Reihenfolge für Lese- und Schreibvorgänge aller flüchtigen Variablen (diese globale Reihenfolge für flüchtige Variablen ist eine Teilreihenfolge gegenüber der größeren Synchronisationsreihenfolge (die eine Gesamtreihenfolge über alle Synchronisationsaktionen ist )). Dies impliziert, dass jeder Thread, der auf ein flüchtiges Feld zugreift, seinen aktuellen Wert liest, bevor er fortfährt, anstatt (möglicherweise) einen zwischengespeicherten Wert zu verwenden. (Es gibt jedoch keine Garantie für die relative Reihenfolge flüchtiger Lese- und Schreibvorgänge mit regulären Lese- und Schreibvorgängen, was bedeutet, dass es sich im Allgemeinen nicht um ein nützliches Threading-Konstrukt handelt.)
- In Java 5 oder höher stellen flüchtige Lese- und Schreibvorgänge eine Ereignis-before-Beziehung her , ähnlich wie das Erfassen und Freigeben eines Mutex.
Die Verwendung ist volatilemöglicherweise schneller als eine Sperre , funktioniert jedoch in einigen Situationen vor Java 5 nicht. Der Bereich der Situationen, in denen volatile wirksam ist, wurde in Java 5 erweitert; insbesondere funktioniert die doppelt geprüfte Verriegelung jetzt korrekt.
In C#
In C # , volatilestellt sicher , dass das Code - Feld ist nicht Gegenstand einiger gewinde unsicherer Optimierungen , die durch den Compiler durchgeführt werden, die CLR, oder durch die Hardware zugreifen können. Wenn ein Feld markiert ist volatile, wird der Compiler angewiesen, eine "Speicherbarriere" oder einen "Zaun" darum herum zu erzeugen, die eine Neuordnung von Befehlen oder eine an das Feld gebundene Zwischenspeicherung verhindert. Beim Lesen eines volatileFelds generiert der Compiler einen Acquire-Fence , der verhindert, dass andere Lese- und Schreibvorgänge in das Feld, einschließlich solcher in anderen Threads, vor den Fence verschoben werden . Beim Schreiben in ein volatileFeld generiert der Compiler einen Release-Fence ; Dieser Zaun verhindert, dass andere Lese- und Schreibvorgänge in das Feld nach dem Zaun verschoben werden .
Nur die folgenden Typen können markiert werden volatile: alle Verweistypen, Single, Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Char, und alle Aufzählungstypen mit dem zugrunde liegenden Typ Byte, SByte, Int16, UInt16, Int32, oder UInt32. (Dies schließt Wert structs , sowie die primitiven Typen Double, Int64, UInt64und Decimal.)
Die Verwendung des volatileSchlüsselworts unterstützt keine Felder, die als Referenz übergeben werden, oder erfasste lokale Variablen ; in diesen Fällen Thread.VolatileReadund Thread.VolatileWritemuss stattdessen verwendet werden.
Tatsächlich deaktivieren diese Methoden einige Optimierungen, die normalerweise vom C#-Compiler, dem JIT-Compiler oder der CPU selbst durchgeführt werden. Die Garantien von Thread.VolatileReadund Thread.VolatileWritesind eine Obermenge der Garantien des volatileSchlüsselworts: statt einen "Half-Fence" zu erzeugen (dh ein Acquire-Fence verhindert nur die Neuordnung von Anweisungen und das Caching davor), VolatileReadund VolatileWriteerzeugen Sie einen "Full-Fence", der verhindern die Neuordnung von Anweisungen und das Zwischenspeichern dieses Felds in beide Richtungen. Diese Methoden funktionieren wie folgt:
- Die
Thread.VolatileWriteMethode erzwingt, dass der Wert im Feld zum Zeitpunkt des Aufrufs geschrieben wird. Außerdem müssen alle früheren Lade- und Speichervorgänge in Programmreihenfolge vor dem Aufruf anVolatileWriteund alle späteren Lade- und Speichervorgänge in Programmreihenfolge nach dem Aufruf erfolgen. - Die
Thread.VolatileReadMethode erzwingt, dass der Wert im Feld zum Zeitpunkt des Aufrufs gelesen wird. Außerdem müssen alle früheren Lade- und Speichervorgänge in Programmreihenfolge vor dem Aufruf anVolatileReadund alle späteren Lade- und Speichervorgänge in Programmreihenfolge nach dem Aufruf erfolgen.
Die Thread.VolatileReadand- Thread.VolatileWriteMethoden generieren einen vollständigen Zaun, indem sie die Thread.MemoryBarrierMethode aufrufen , die eine Speicherbarriere erstellt, die in beide Richtungen funktioniert. Zusätzlich zu den oben angegebenen Motivationen für die Verwendung eines vollständigen Zauns ist ein potenzielles Problem mit dem volatileSchlüsselwort, das durch die Verwendung eines vollständigen Zauns gelöst wird, der von erzeugt Thread.MemoryBarrierwird, wie folgt: Aufgrund der asymmetrischen Natur von Halbzäunen wird ein volatileFeld mit einem Schreibbefehl gefolgt von bei einem Lesebefehl kann die Ausführungsreihenfolge immer noch vom Compiler getauscht werden. Da Vollzäune symmetrisch sind, ist dies bei der Verwendung kein Problem Thread.MemoryBarrier.
In Fortran
VOLATILEist Teil des Fortran 2003- Standards, obwohl eine frühere Version ihn als Erweiterung unterstützte. Das Erstellen aller Variablen volatilein einer Funktion ist auch nützlich, um Aliasing- bezogene Fehler zu finden.
integer, volatile :: i ! When not defined volatile the following two lines of code are identical
write(*,*) i**2 ! Loads the variable i once from memory and multiplies that value times itself
write(*,*) i*i ! Loads the variable i twice from memory and multiplies those values
Durch immer "Drilldown" in den Speicher eines VOLATILE wird der Fortran-Compiler daran gehindert, Lese- oder Schreibvorgänge auf flüchtige Elemente neu zu ordnen. Dadurch werden Aktionen in diesem Thread für andere Threads sichtbar und umgekehrt.
Der Einsatz von VOLATILE reduziert die Optimierung und kann sie sogar verhindern.