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++
[edytuj | edytuj kod]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
setjmp
ilongjmp
- pozwalać na używanie zmiennych
sig_atomic_t
w 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
[edytuj | edytuj kod]
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
[edytuj | edytuj kod]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
[edytuj | edytuj kod]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
volatile
zachowują 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
[edytuj | edytuj kod]- ↑ a b 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.).
- ↑ a b 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] .