ulotne (programowanie komputerowe) - volatile (computer programming)
W programowaniu komputerowym , szczególnie w językach programowania C , C++ , C# i Java , słowo kluczowe volatile wskazuje, że wartość może się zmieniać między różnymi dostępami, nawet jeśli nie wydaje się, aby została zmodyfikowana. To słowo kluczowe uniemożliwia kompilatorowi optymalizującemu optymalizację kolejnych odczytów lub zapisów, a tym samym niepoprawne ponowne użycie nieaktualnej wartości lub pominięcie zapisów. Wartości ulotne pojawiają się przede wszystkim w dostępie do sprzętu ( wejścia/wyjścia mapowane w pamięci ), gdzie odczyt lub zapis do pamięci jest używany do komunikacji z urządzeniami peryferyjnymi oraz w wątkach , gdzie inny wątek mógł zmodyfikować wartość.
Pomimo tego, że jest powszechnym słowem kluczowym, zachowanie volatile
różni się znacznie w zależności od języka programowania i łatwo jest go źle zrozumieć. W C i C++ jest to kwalifikator typu , jak const
i jest właściwością typu . Co więcej, w C i C++ nie działa w większości scenariuszy wątków, a takie użycie jest odradzane. W Javie i C# jest to właściwość zmiennej i wskazuje, że obiekt, z którym powiązana jest zmienna, może ulec mutacji i jest specjalnie przeznaczony do obsługi wątków. W języku programowania D istnieje osobne słowo kluczowe shared
dla użycia wątków, ale nie volatile
istnieje żadne słowo kluczowe.
W C i C++
W C, a co za tym idzie C++, volatile
słowo kluczowe miało na celu:
- zezwól na dostęp do mapowanych w pamięci urządzeń we/wy
- zezwól na użycie zmiennych między
setjmp
ilongjmp
- pozwalają na użycie
sig_atomic_t
zmiennych w procedurach obsługi sygnałów.
Chociaż zamierzone zarówno przez C, jak i C++, standardy C nie wyrażają, że volatile
semantyka odnosi się do l-wartości, a nie do obiektu, do którego się odwołuje. Odpowiedni raport o defektach DR 476 (do C11) jest nadal analizowany z C17.
Operacje na volatile
zmiennych nie są atomowe ani nie ustanawiają właściwej relacji „zdarzy się przed” dla wątków. Jest to określone w odpowiednich standardach (C, C++, POSIX , WIN32), a zmienne nietrwałe nie są bezpieczne wątkowo w zdecydowanej większości obecnych implementacji. Dlatego też użycie volatile
słowa kluczowego jako przenośnego mechanizmu synchronizacji jest odradzane przez wiele grup C/C++.
Przykład odwzorowania we/wy w pamięci w C
W tym przykładzie, kod ustawia wartość przechowywaną w foo
celu 0
. Następnie zaczyna wielokrotnie odpytywać tę wartość, aż zmieni się na 255
:
static int foo;
void bar(void) {
foo = 0;
while (foo != 255)
;
}
Optymalizacji kompilator nie zauważy, że żaden inny kod może ewentualnie zmienić wartość przechowywaną w foo
, i zakłada, że pozostanie ona równa 0
w każdym czasie. Kompilator zastąpi zatem ciało funkcji nieskończoną pętlą podobną do tej:
void bar_optimized(void) {
foo = 0;
while (true)
;
}
Jednak foo
może reprezentować lokalizację, która może być zmieniana przez innych elementów systemu komputerowego w każdej chwili, takich jak rejestru sprzętowego urządzenia podłączonego do procesora . Powyższy kod nigdy nie wykryłby takiej zmiany; bez volatile
słowa kluczowego kompilator zakłada, że bieżący program jest jedyną częścią systemu, która może zmienić wartość (co jest zdecydowanie najczęstszą sytuacją).
Aby uniemożliwić kompilatorowi optymalizację kodu jak powyżej, volatile
używane jest słowo kluczowe:
static volatile int foo;
void bar (void) {
foo = 0;
while (foo != 255)
;
}
Dzięki tej modyfikacji stan pętli nie zostanie zoptymalizowany, a system wykryje zmianę, gdy się pojawi.
Ogólnie rzecz biorąc, istnieją operacje bariery pamięci dostępne na platformach (ujawnione w C++11), które powinny być preferowane zamiast nietrwałych, ponieważ pozwalają kompilatorowi na lepszą optymalizację i, co ważniejsze, gwarantują poprawne zachowanie w scenariuszach wielowątkowych; ani specyfikacja C (przed C11), ani specyfikacja C++ (przed C++11) nie określają modelu pamięci wielowątkowej, więc volatile może nie zachowywać się deterministycznie pomiędzy systemami operacyjnymi/kompilatorami/CPU.
Porównanie optymalizacji w C
Poniższe programy w języku C i towarzyszące im zestawy pokazują, jak volatile
słowo kluczowe wpływa na dane wyjściowe kompilatora. Kompilatorem w tym przypadku był GCC .
Obserwując kod asemblera wyraźnie widać, że kod generowany z volatile
obiektami jest bardziej rozwlekły, przez co wydłuża się, aby volatile
można było spełnić charakter obiektów. W volatile
zapobiega słów kluczowych kompilator z wykonywania optymalizacja kodu udziałem lotnych obiektów, zapewniając w ten sposób, że każda zmienna i niestabilna przypisanie odczytu posiada odpowiedni dostęp do pamięci. Bez volatile
słowa kluczowego kompilator wie, że zmienna nie musi być ponownie odczytywana z pamięci przy każdym użyciu, ponieważ nie powinno być żadnych zapisów do jego lokalizacji w pamięci z żadnego innego wątku lub procesu.
Porównanie montażu | |
---|---|
Bez volatile słowa kluczowego |
Ze volatile słowem kluczowym
|
# 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 bez.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
Zgodnie ze standardem ISO C++11 , słowo kluczowe volatile jest przeznaczone tylko do dostępu do sprzętu; nie używaj go do komunikacji między wątkami. Do komunikacji między wątkami biblioteka standardowa udostępnia std::atomic<T>
szablony.
w języku Java
Język programowania Java również zawiera volatile
słowo kluczowe, ale jest ono używane w nieco innym celu. Po zastosowaniu do pola kwalifikator Java volatile
zapewnia następujące gwarancje:
- We wszystkich wersjach Javy istnieje globalne uporządkowanie odczytów i zapisów wszystkich zmiennych lotnych (to globalne uporządkowanie zmiennych niestabilnych jest porządkiem częściowym względem większego porządku synchronizacji (który jest porządkiem całkowitym dla wszystkich działań synchronizacji )). Oznacza to, że każdy wątek uzyskujący dostęp do pola ulotnego odczyta swoją bieżącą wartość przed kontynuowaniem, zamiast (potencjalnie) używać wartości z pamięci podręcznej. (Nie ma jednak gwarancji co do względnej kolejności niestabilnych odczytów i zapisów przy regularnych odczytach i zapisach, co oznacza, że generalnie nie jest to przydatna konstrukcja wątkowa).
- W Javie 5 lub nowszych, niestabilne odczyty i zapisy ustanawiają relację „zdarzy się przed” , podobnie jak pozyskiwanie i zwalnianie muteksu.
Używanie volatile
może być szybsze niż lock , ale nie będzie działać w niektórych sytuacjach przed Javą 5. Zakres sytuacji, w których volatile jest skuteczny, został rozszerzony w Javie 5; w szczególności podwójne sprawdzanie blokowania działa teraz poprawnie.
W C#
W języku C # , volatile
zapewnia, że kod dostępu pola nie podlega niektórych optymalizacje nitki niebezpieczne, które mogą być wykonywane przez kompilator, CLR lub sprzętowo. Gdy pole jest oznaczone volatile
, kompilator otrzymuje polecenie wygenerowania „bariery pamięci” lub „ogrodzenia” wokół niego, co uniemożliwia zmianę kolejności instrukcji lub buforowanie powiązane z polem. Podczas odczytywania volatile
pola kompilator generuje ogrodzenie nabywania , które zapobiega przenoszeniu innych odczytów i zapisów w polu, w tym w innych wątkach, przed ogrodzenie. Podczas zapisywania do volatile
pola kompilator generuje ograniczenie zwolnienia ; Zapobiega to ogrodzenie inny odczytuje i zapisuje do pola z przemieszczane po ogrodzeniu.
Tylko następujące typy mogą być oznakowane volatile
: wszystkie typy referencyjne, Single
, Boolean
, Byte
, SByte
, Int16
, UInt16
, Int32
, UInt32
, Char
, i wszystkie typy wyliczeniowe z bazowego typu Byte
, SByte
, Int16
, UInt16
, Int32
, lub UInt32
. (Z wyłączeniem wartość konstrukcjom , jak również pierwotne typu Double
, Int64
, UInt64
i Decimal
).
Użycie volatile
słowa kluczowego nie obsługuje pól, które są przekazywane przez referencję lub przechwyconych zmiennych lokalnych ; w takich przypadkach Thread.VolatileRead
i Thread.VolatileWrite
musi być używany zamiast tego.
W efekcie metody te wyłączają niektóre optymalizacje zwykle wykonywane przez kompilator C#, kompilator JIT lub sam procesor. Gwarancje zapewniane przez Thread.VolatileRead
i Thread.VolatileWrite
są nadzbiorem gwarancji zapewnianych przez volatile
słowo kluczowe: zamiast generowania „pół ogrodzenia” (tj. granica nabycia uniemożliwia jedynie zmianę kolejności instrukcji i buforowanie, która jest przed nim) VolatileRead
i VolatileWrite
generuje „pełne ogrodzenie”, które zapobiec zmianie kolejności instrukcji i buforowaniu tego pola w obu kierunkach. Metody te działają w następujący sposób:
- Te
Thread.VolatileWrite
siły metoda wartość w polu, aby zostać zapisane w punkcie połączenia. Ponadto, wszelkie wcześniejsze wczytywanie i zapisywanie kolejności programu musi nastąpić przed wywołaniem,VolatileWrite
a wszelkie późniejsze wczytywanie i zapisywanie kolejności programu musi nastąpić po wywołaniu. - Te
Thread.VolatileRead
siły metoda wartość w polu należy czytać od w punkcie połączenia. Ponadto, wszelkie wcześniejsze wczytywanie i zapisywanie kolejności programu musi nastąpić przed wywołaniem,VolatileRead
a wszelkie późniejsze wczytywanie i zapisywanie kolejności programu musi nastąpić po wywołaniu.
Thread.VolatileRead
I Thread.VolatileWrite
metody generowania pełny płot wywołując Thread.MemoryBarrier
metodę, która konstruuje barierę pamięci, który działa w obu kierunkach. Oprócz motywów stosowania pełnego ogrodzenia podanych powyżej, jeden potencjalny problem ze volatile
słowem kluczowym rozwiązywanym za pomocą pełnego ogrodzenia generowanego przez Thread.MemoryBarrier
jest następujący: ze względu na asymetryczny charakter półogrodzenia volatile
pole z instrukcją zapisu, po którym następuje instrukcja read może nadal mieć zamienioną kolejność wykonania przez kompilator. Ponieważ pełne ogrodzenia są symetryczne, nie stanowi to problemu przy użyciu Thread.MemoryBarrier
.
W Fortranie
VOLATILE
jest częścią standardu Fortran 2003 , chociaż wcześniejsza wersja obsługiwała go jako rozszerzenie. Wykonywanie wszystkich zmiennych volatile
w funkcji jest również przydatna stwierdzenie aliasing związanych błędów.
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
Zawsze „drążając” do pamięci VOLATILE, kompilator Fortran nie może zmieniać kolejności odczytów lub zapisów na lotne. Dzięki temu akcje wykonywane w tym wątku są widoczne dla innych wątków i na odwrót.
Użycie VOLATILE zmniejsza, a nawet może uniemożliwić optymalizację.