Zmienna ulotna
Zmienna ulotna – zmienna lub obiekt, które mogą zostać zmienione "z zewnątrz" — niezależnie od kodu programu, w którym się znajdują.
Pomiędzy różnymi odczytami, wartości zmiennej mogą być różne, nawet jeśli nie były zmodyfikowane w kodzie. Zastosowanie volatile powstrzymuje kompilator optymalizujący przed pomijaniem zapisów do pamięci lub w wypadku kolejnych odczytów czy zapisów zmiennej przed zastąpieniem jej w skompilowanym kodzie przez stałą. Zmienne ulotne pojawiają się przede wszystkim w dostępie do sprzętu, gdzie korzystanie z pamięci jest wykorzystywane do komunikacji pomiędzy urządzeniami oraz w środowisku wielowątkowym, w którym różne wątki mogą korzystać z tej samej zmiennej.
Pomimo bycia powszechnym słowem kluczowym dokładne zachowanie volatile różni się pomiędzy językami programowania. W C i C++ jest modyfikatorem do typu podobnie jak słowo kluczowe const i nie sprawdza się w większości szablonów programów wielowątkowych, dlatego jego zastosowanie jest odradzane. W językach C# i Java jest przeznaczone specjalnie do wielowątkowości — charakteryzuje zmienną i oznacza, że obiekt, z którym jest ona powiązana, może się zmienić.
C oraz C++
W C i następnie w C++ słowo volatile miało spełniać następujące założenia:[1]
- dawać dostęp do urządzeń MMIO
- pozwalać na korzystanie ze zmiennych pomiędzy
setjmpilongjmp - pozwalać na używanie zmiennych
sig_atomic_tw procedurach obsługi przerwań (ang. signal handlers)
Operacje na zmiennych ulotnych nie są operacjami atomowymi, ani też nie ustanawiają prawidłowiej relacji happens-before (określa w jakiej względnej kolejności wykonywane są instrukcje). Jest to określone w odpowiednich standardach (C, C++, POSIX, WIN32)[1]. Zmienne ulotne nie są bezpieczne dla zdecydowanej większości dzisiejszych implementacji programów wielowątkowych, dlatego też użycie volatile jako przenośnego mechanizmu synchronizacji jest odradzane[2][3].
Przykład zastosowania w C
Poniższy kod inicjuje zmienną foo na 0 i wykonuje pętlę while dopóki foo nie jest równe 255:
static int foo;
void var(void) {
foo = 0;
while (foo != 255){
/* ... */
}
}
Kompilator optymalizujący zauważy, że żaden inny kod nie może zmienić wartości foo, dlatego założy, że pozostanie ona równa 0. Zamieni wtedy warunek wewnątrz pętli na:
static int foo;
void var(void) {
foo = 0;
while (true){
/* ... */
}
}
Problem pojawia się, gdy foo reprezentuje na przykład pewną lokację w pamięci, która może być zmieniona przez inne elementy systemu w dowolnej chwili (np. rejestr urządzeń lub CPU). Powyższy kod nigdy nie wykryłby takiej zmiany — bez volatile kompilator zakłada, że tylko bieżący program może zmienić wartość foo.
Aby zapobiec intruzyjnej optymalizacji kompilatora należy zastosować volatile w następujący sposób:
static volatile int foo;
void var(void) {
foo = 0;
while (foo != 255){
/* ... */
}
}
W powyższym wypadku kompilator wygeneruje kod, który za każdym razem, gdy będziemy próbowali odczytać wartość zmiennej foo, załaduje jej wartość z oryginalnego miejsca w pamięci (zamiast chociażby korzystać z wartości zapisanej w pamięci cache). Mechanizm ten jest często wykorzystywany w systemach wbudowanych do pobierania danych ze sprzętowych modułów wbudowanych w mikrokontrolery (np. przetwornika analogowo-cyfrowego)[4].
Na większości dzisiejszych platform istnieje system bariery pamięci (od C++11), który powinien być wykorzystywany zamiast mechanizmu volatile, ponieważ pozwala kompilatorowi na lepszą optymalizację i zapewnia poprawne zachowanie podczas operacji wielowątkowych; zarówno C (przed C11), jak i C++ (przed C++11) zakładają modelu wielowątkowego dostępu do pamięci, dlatego zachowanie volatile może nie być deterministyczne pomiędzy kompilatorami/procesorami/systemami operacyjnymi[5].
C++11
Według standardu C++11 ISO słowo kluczowe volatile jest przeznaczone jedynie dla dostępu sprzętowego i nie należy go używać do komunikacji między wątkami — do tego biblioteka STL przeznaczyła szablony std::atomic<T>[3].
Java
Język Java również posiada słowo kluczowe volatile, lecz ma ona nieco inną specyfikację:
- We wszystkich wersjach Javy istnieje globalna kolejność odczytów i zapisów do zmiennej z
volatile. Dzięki temu każdy wątek mający do niej dostęp odczyta jej obecną wartość przed kontynuacją, zamiast (potencjalnego) wykorzystania zmiennej przechowywanej w pamięci podręcznej. - Od Javy 5 zmienne z modyfikatorem
volatilezachowują relację happens-before.
Używanie volatile może być szybsze niż blokowanie, ale może też nie działać w niektórych wypadkach[6][7].
Przypisy
- 1 2 Should volatile acquire atomicity and thread visibility semantics? [online], www.open-std.org [dostęp 2017-09-03].
- ↑ Why the “volatile” type class should not be used — The Linux Kernel documentation [online], www.kernel.org [dostęp 2017-09-03] (ang.).
- 1 2 volatile (C++) [online], msdn.microsoft.com [dostęp 2017-09-03] (ang.).
- ↑ Arduino reference [online] [dostęp 2019-01-20] [zarchiwizowane z adresu 2019-01-21] (ang.).
- ↑ Linux: Volatile Superstition [online] [zarchiwizowane z adresu 2010-06-20].
- ↑ Fastest Thread-safe Singleton in Java | Literate Java [online], literatejava.com [dostęp 2017-09-03] (ang.).
- ↑ Neil Coffey, Double-checked Locking [online], www.javamex.com [dostęp 2017-09-03].