Zmienna ulotna

Z Wikipedii, wolnej encyklopedii

Zmienna ulotnazmienna 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]

Operacje na zmiennych ulotnych nie są operacjami atomowymi, ani też nie ustanawiają prawidłowiej relacji happens-before(inne języki) (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(inne języki).

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]

  1. a b Should volatile acquire atomicity and thread visibility semantics? [online], www.open-std.org [dostęp 2017-09-03].
  2. Why the “volatile” type class should not be used — The Linux Kernel documentation [online], www.kernel.org [dostęp 2017-09-03] (ang.).
  3. a b volatile (C++) [online], msdn.microsoft.com [dostęp 2017-09-03] (ang.).
  4. Arduino reference [online] [dostęp 2019-01-20] [zarchiwizowane z adresu 2019-01-21] (ang.).
  5. Linux: Volatile Superstition [online] [zarchiwizowane z adresu 2010-06-20].
  6. Fastest Thread-safe Singleton in Java | Literate Java [online], literatejava.com [dostęp 2017-09-03] (ang.).
  7. Neil Coffey, Double-checked Locking [online], www.javamex.com [dostęp 2017-09-03].