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 a longjmp
  • 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.

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łaniem VolatileWrite , 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łaniem VolatileRead , 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ę.

Linki zewnętrzne