ulotny (programowanie komputerowe)
W programowaniu komputerowym zmienność oznacza , że wartość jest podatna na zmiany w czasie, poza kontrolą jakiegoś kodu. Zmienność ma wpływ na konwencje wywoływania funkcji , a także wpływa na sposób przechowywania, uzyskiwania dostępu i buforowania zmiennych.
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ę być modyfikowana. To słowo kluczowe uniemożliwia kompilatorowi optymalizującemu optymalizację kolejnych odczytów lub zapisów, a tym samym nieprawidłowe ponowne użycie nieaktualnej wartości lub pominięcie zapisów. Wartości ulotne pojawiają się przede wszystkim w dostępie sprzętowym ( wejścia/wyjścia mapowane w pamięci ), gdzie odczyt lub zapis do pamięci jest używany do komunikacji z urządzeń peryferyjnych i wątków , gdzie inny wątek mógł zmodyfikować wartość.
Pomimo tego, że jest to powszechne słowo kluczowe, zachowanie volatile
różni się znacznie w zależności od języka programowania i łatwo jest źle rozumiane. W C i C++ jest kwalifikatorem typu , podobnie jak const
, i jest właściwością typu . Ponadto w C i C++ nie działa w większości scenariuszy wątków i odradza się takie użycie. W Javie i C# jest to właściwość zmiennej i wskazuje, że obiekt , z którym powiązana jest zmienna, może mutować i jest specjalnie przeznaczony do wątków. w D język programowania, istnieje osobne słowo kluczowe wspólne
dla użycia wątków, ale nie istnieje słowo kluczowe ulotne .
W C i C++
W C, a co za tym idzie w C++, słowo kluczowe volatile
miało na celu
- umożliwiają dostęp do urządzeń we/wy mapowanych w pamięci
- zezwalaj na użycie zmiennych między
setjmp
alongjmp
- zezwalaj na użycie zmiennych
sig_atomic_t
w procedurach obsługi sygnałów.
Choć zamierzone zarówno w C, jak i C++, standardy C nie wyrażają, że zmienna
semantyka odnosi się do lwartości, a nie do obiektu, do którego się odwołuje. Odpowiedni raport o defektach DR 476 (do C11) jest nadal analizowany przez C17 .
Operacje na zmiennych lotnych
nie są atomowe ani nie ustanawiają właściwej relacji dzieje się przed wątkami. Jest to określone w odpowiednich standardach (C, C++, POSIX , WIN32), a zmienne lotne nie są wątkowo bezpieczne w zdecydowanej większości obecnych implementacji. W związku z tym wiele grup C/C++ odradza używanie słowa kluczowego volatile jako przenośnego mechanizmu synchronizacji.
Przykład operacji we/wy mapowanych w pamięci w języku C
0
W tym przykładzie kod ustawia wartość przechowywaną w foo
na . Następnie zaczyna sondować tę wartość, aż zmieni się na 255
:
0
statyczny int foo ; pusty słupek ( pusty ) { foo = ; while ( foo != 255 ) ; }
0
Kompilator optymalizujący zauważy, że żaden inny kod nie może zmienić wartości przechowywanej w foo
i założy, że pozostanie ona równa przez cały czas. Dlatego kompilator zastąpi treść funkcji nieskończoną pętlą podobną do tej:
0
void bar_optimized ( void ) { foo = ; podczas gdy ( prawda ) ; }
Jednak foo
może reprezentować lokalizację, która może być zmieniona przez inne elementy systemu komputerowego w dowolnym momencie, na przykład rejestr sprzętowy urządzenia podłączonego do procesora . Powyższy kod nigdy nie wykryłby takiej zmiany; bez volatile
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 w sposób opisany powyżej, używane jest słowo kluczowe volatile :
0
static volatile int foo ; pusty słupek ( pusty ) { foo = ; while ( foo != 255 ) ; }
Dzięki tej modyfikacji stan pętli nie zostanie zoptymalizowany, a system wykryje zmianę, gdy ona nastąpi.
Ogólnie rzecz biorąc, na platformach dostępne są operacje bariery pamięci (które są widoczne w C++ 11), które powinny być preferowane zamiast ulotnych, ponieważ pozwalają kompilatorowi na lepszą optymalizację, a co ważniejsze, gwarantują poprawne zachowanie w scenariuszach wielowątkowych; ani specyfikacja C (przed C11), ani specyfikacja C++ (przed C++11) nie określa modelu pamięci wielowątkowej, więc ulotność może nie zachowywać się deterministycznie w różnych systemach operacyjnych/kompilatorach/procesorach.
Porównanie optymalizacji w C
Poniższe programy 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 obiektami ulotnymi jest bardziej rozwlekły, przez co jest dłuższy, aby
można było spełnić naturę obiektów ulotnych .
Słowo kluczowe volatile
uniemożliwia kompilatorowi wykonanie optymalizacji kodu obejmującego obiekty ulotne, zapewniając w ten sposób, że każde przypisanie i odczyt zmiennej ulotnej ma odpowiedni dostęp do pamięci. Bez ulotności
słowo kluczowe, 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 jej lokalizacji w pamięci z żadnego innego wątku lub procesu.
Porównanie montażu | |
---|---|
Bez niestabilnych słów kluczowych |
Ze zmiennym słowem kluczowym |
0 0
# include <stdio.h> int main () { /* Te zmienne nigdy nie zostaną utworzone na stosie*/ int a = 10 , b = 100 , c = , d = ; /* "printf" zostanie wywołane z argumentami "%d" i 110 (kompilator oblicza sumę a+b), stąd nie ma narzutu związanego z wykonywaniem dodawania w czasie wykonywania */ printf ( "%d" , a
0
+ b ); /* Ten kod zostanie usunięty poprzez optymalizację, ale wpływ zmiany wartości „c” i „d” na 100 można zobaczyć podczas wywoływania „printf” */ a = b ; do = b ; re = b ; /* Kompilator wygeneruje kod, w którym wywołany zostanie printf z argumentami "%d" i 200 */ printf ( "%d" , c + d ); powrót ; }
|
0 0
# zawierać <stdio.h> int main () { volatile int a = 10 , b = 100 , c = , d = ; printf ( "%d" , a + b ); za = b ; do = b ; re = b ; printf ( "%d" , c
0
+ re ); powrót ; }
|
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 ruchów esi , 110 ruchów edi , OFFSET FLAT :. LC0 xor eax , eax call printf mov esi , 200 mov edi , OFFSET FLAT :. LC0 xor eax , eax wywołanie printf xor eax , eax dodaj 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 ,
0
0
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 ], mov DWORD PTR [ rsp + 12 ], 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" .sekcja .note.GNU-stack , "" , @progbits
|
C++11
Zgodnie ze standardem ISO C++ 11 słowo kluczowe volatile służy wyłącznie do uzyskiwania dostępu do sprzętu; nie używaj go do komunikacji między wątkami. Na potrzeby komunikacji między wątkami standardowa biblioteka udostępnia std::atomic<T>
.
w Jawie
Język programowania Java ma również słowo kluczowe volatile
, 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 ulotnych (to globalne uporządkowanie zmiennych ulotnych jest porządkiem częściowym w stosunku do większego porządku synchronizacji ( który jest porządkiem całkowitym we wszystkich akcjach synchronizacji )). Oznacza to, że każdy wątek uzyskujący dostęp do pola niestabilnego odczyta jego bieżącą wartość przed kontynuowaniem, zamiast (potencjalnie) używać wartości z pamięci podręcznej. (Jednak nie ma gwarancji co do względnego uporządkowania ulotnych odczytów i zapisów ze zwykłymi odczytami i zapisami, co oznacza, że generalnie nie jest to użyteczna konstrukcja wątków).
- W Javie 5 lub nowszej ulotne odczyty i zapisy ustanawiają relację „zdarza się przed” , podobnie jak uzyskiwanie 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 języku C#
W języku C# volatile zapewnia
, że kod uzyskujący dostęp do pola nie podlega pewnym optymalizacjom niebezpiecznym dla wątków, które mogą być wykonywane przez kompilator, środowisko CLR lub sprzęt. Gdy pole jest oznaczone volatile
, kompilator otrzymuje polecenie wygenerowania wokół niego „bariery pamięci” lub „ogrodzenia”, co zapobiega zmianie kolejności instrukcji lub buforowaniu powiązanemu z polem. Podczas odczytu ulotnego
kompilator generuje zabezpieczenie przed przejęciem , które zapobiega przeniesieniu innych odczytów i zapisów do pola, w tym w innych wątkach, przed płot. Podczas zapisywania w ulotnym
kompilator generuje komunikat o wyzwoleniu ; to ogrodzenie zapobiega przesuwaniu innych odczytów i zapisów w polu za ogrodzeniem.
Tylko następujące typy mogą być oznaczone jako nietrwałe
: wszystkie typy referencyjne, Single
, Boolean
, Byte
, SByte
, Int16
, UInt16
, Int32
, UInt32
, Char
i wszystkie typy wyliczeniowe z typem bazowym Byte
, SByte
, Int16
, UInt16
, Int32
, lub UInt32
. (Wyklucza to struktury wartości , a także typy pierwotne Double
, Int64
, UInt64
i Decimal
.)
Użycie słowa kluczowego volatile
nie obsługuje pól przekazywanych przez odwołanie lub przechwyconych zmiennych lokalnych ; w takich przypadkach należy użyć Thread.VolatileRead
i Thread.VolatileWrite .
W efekcie metody te wyłączają niektóre optymalizacje zwykle wykonywane przez kompilator języka C#, kompilator JIT lub sam procesor. Gwarancje zapewniane przez Thread.VolatileRead
i Thread.VolatileWrite
są nadzbiorem gwarancji zapewnianych przez słowo kluczowe volatile
: zamiast generować „półogrodzenie” (tj .
i VolatileWrite
wygenerować „pełne ogrodzenie”, które zapobiega zmianie kolejności instrukcji i buforowaniu tego pola w obu kierunkach. Metody te działają w następujący sposób:
- Metoda
Thread.VolatileWrite
wymusza zapisanie wartości w polu w momencie wywołania. Ponadto wszelkie wcześniejsze ładowanie i zapisywanie kolejności programów musi nastąpić przed wywołaniemVolatileWrite
, a wszelkie późniejsze ładowanie i zapisywanie kolejności programów musi nastąpić po wywołaniu. - Metoda
Thread.VolatileRead
wymusza odczytanie wartości w polu w momencie wywołania. Ponadto wszelkie wcześniejsze ładowanie i zapisywanie kolejności programów musi nastąpić przed wywołaniemVolatileRead
, a wszelkie późniejsze ładowanie i zapisywanie kolejności programów musi nastąpić po wywołaniu.
Thread.VolatileRead i Thread.VolatileWrite generują pełne ogrodzenie
,
wywołując metodę Thread.MemoryBarrier
, która tworzy barierę pamięci działającą w obu kierunkach. Oprócz podanych powyżej motywacji do używania pełnego ogrodzenia, jeden potencjalny problem ze volatile
, który można rozwiązać za pomocą pełnego ogrodzenia wygenerowanego przez Thread.MemoryBarrier
, jest następujący: ze względu na asymetryczną naturę półogrodzenia, niestabilność
pole z instrukcją zapisu, po której następuje instrukcja odczytu, może nadal mieć zamienioną kolejność wykonywania przez kompilator. Ponieważ pełne ogrodzenia są symetryczne, nie stanowi to problemu w przypadku używania Thread.MemoryBarrier
.
W Fortranie
VOLATILE
jest częścią standardu Fortran 2003 , chociaż wcześniejsza wersja obsługiwała go jako rozszerzenie. Uczynienie wszystkich zmiennych niestabilnymi
w funkcji jest również przydatne przy wyszukiwaniu błędów związanych z aliasingiem .
liczba całkowita , ulotna :: i ! Gdy nie zdefiniowano volatile, następujące dwie linie kodu są identyczne write ( * , * ) i ** 2 ! Ładuje zmienną i raz z pamięci i mnoży tę wartość razy samą wartość write ( * , * ) i * i ! Ładuje zmienną i dwukrotnie z pamięci i mnoży te wartości
Zawsze „drążąc” do pamięci VOLATILE, kompilator Fortran nie może zmieniać kolejności odczytów ani zapisów do lotnych. Dzięki temu działania wykonane w tym wątku są widoczne dla innych wątków i odwrotnie.
Użycie VOLATILE zmniejsza, a nawet może uniemożliwić optymalizację.