volatile (datorprogrammering)

Inom datorprogrammering betyder volatile att ett värde är benäget att förändras över tid , utanför kontroll av någon kod. Volatilitet har implikationer inom funktionsanropskonventioner och påverkar också hur variabler lagras, nås och cachelagras.

I programmeringsspråken C , C++ , C# och Java indikerar det flyktiga nyckelordet att ett värde kan ändras mellan olika åtkomster, även om det inte verkar vara modifierat. Detta nyckelord förhindrar en optimerande kompilator från att optimera bort efterföljande läsningar eller skrivningar och därmed felaktigt återanvända ett inaktuellt värde eller utelämna skrivningar. Volatila värden uppstår främst vid hårdvaruåtkomst ( minnesmappad I/O ), där läsning från eller skrivning till minne används för att kommunicera med kringutrustning, och vid trådning , där en annan tråd kan ha ändrat ett värde.

Trots att det är ett vanligt nyckelord, skiljer sig beteendet hos volatile avsevärt mellan programmeringsspråk och är lätt att missförstå. I C och C++ är det en typkvalificerare , som const , och är en egenskap av typen . Dessutom fungerar det inte i C och C++ i de flesta trådningsscenarier, och den användningen avråds. I Java och C# är det en egenskap hos en variabel och indikerar att objektet som variabeln är bunden till kan mutera, och är specifikt avsett för trådning. I D finns det ett separat nyckelord som delas för trådningsanvändningen, men det finns inget flyktigt nyckelord.

I C och C++

I C, och följaktligen C++, var det flyktiga nyckelordet avsett att

  • ge åtkomst till minnesmappade I/O- enheter
  • tillåter användning av variabler mellan setjmp och longjmp
  • tillåter användning av sig_atomic_t -variabler i signalhanterare.

Även om de är avsedda av både C och C++, misslyckas C-standarderna att uttrycka att den flyktiga semantiken refererar till lvärdet, inte det refererade objektet. Respektive felanmälan DR 476 (till C11) är fortfarande under granskning med C17 .

Operationer på flyktiga variabler är inte atomära , och de etablerar inte heller ett riktigt händer-före-förhållande för trådning. Detta specificeras i de relevanta standarderna (C, C++, POSIX , WIN32), och flyktiga variabler är inte trådsäkra i de allra flesta nuvarande implementeringar. Användningen av flyktiga nyckelord som en bärbar synkroniseringsmekanism avråds således av många C/C++-grupper.

Exempel på minnesmappad I/O i C

0 I det här exemplet ställer koden in värdet som lagras i foo till . Den börjar sedan polla upprepade gånger tills det ändras till 255 :

  

  
      0

       
         
 statisk  int  foo  ;  void  bar  (  void  )  {  foo  =  ;  while  (  foo  !=  255  )  ;  } 

0 En optimerande kompilator kommer att märka att ingen annan kod möjligen kan ändra värdet som lagras i foo , och kommer att anta att det kommer att förbli lika med hela tiden. Kompilatorn kommer därför att ersätta funktionskroppen med en oändlig loop liknande denna:

  
      0

     
         
 void  bar_optimized  (  void  )  {  foo  =  ;  while  (  sant  )  ;  } 

Foo kan dock representera en plats som kan ändras av andra delar av datorsystemet när som helst, till exempel ett maskinvaruregister för en enhet som är ansluten till CPU:n . Ovanstående kod skulle aldrig upptäcka en sådan förändring; utan det flyktiga nyckelordet antar kompilatorn att det aktuella programmet är den enda delen av systemet som kan ändra värdet (vilket är den absolut vanligaste situationen).

För att förhindra att kompilatorn optimerar koden enligt ovan, används nyckelordet volatile :

   

   
      0

       
        
 statisk  flyktig  int  foo  ;  void  bar  (  void  )  {  foo  =  ;  while  (  foo  !=  255  )  ;  } 

Med denna modifiering kommer slingtillståndet inte att optimeras bort, och systemet kommer att upptäcka förändringen när den inträffar.

Generellt finns det minnesbarriäroperationer tillgängliga på plattformar (som är exponerade i C++11) som bör föredras istället för flyktiga eftersom de tillåter kompilatorn att utföra bättre optimering och ännu viktigare de garanterar korrekt beteende i flertrådade scenarier; varken C-specifikationen (före C11) eller C++-specifikationen (före C++11) specificerar en flertrådig minnesmodell, så flyktigt kanske inte beter sig deterministiskt över OS/kompilatorer/CPU:er.

Optimeringsjämförelse i C

Följande C-program, och medföljande sammansättningar, visar hur det flyktiga nyckelordet påverkar kompilatorns utdata. Kompilatorn i detta fall var GCC .

När man observerar monteringskoden är det tydligt att koden som genereras med flyktiga objekt är mer utförlig, vilket gör den längre så att flyktiga objekts natur kan uppfyllas. Det flyktiga nyckelordet hindrar kompilatorn från att utföra optimering av kod som involverar flyktiga objekt, vilket säkerställer att varje flyktig variabeltilldelning och läsning har motsvarande minnesåtkomst. Utan volatile vet kompilatorn att en variabel inte behöver läsas om från minnet vid varje användning, eftersom det inte borde finnas några skrivningar till dess minnesplats från någon annan tråd eller process.

C++11

Enligt C++11 ISO-standarden är det flyktiga nyckelordet endast avsett för användning för hårdvaruåtkomst; använd den inte för kommunikation mellan trådar. För kommunikation mellan trådar tillhandahåller standardbiblioteket std::atomic<T> -mallar.

I Java

Programmeringsspråket Java har också nyckelordet volatile , men det används för ett något annat syfte. När den tillämpas på ett fält, ger Java qualifier volatile följande garantier:

  • I alla versioner av Java finns det en global ordning på läsning och skrivning av alla flyktiga variabler (denna globala ordning på flyktiga ämnen är en delordning över den större synkroniseringsordningen (som är en total ordning över alla synkroniseringsåtgärder )). Detta innebär att varje tråd som kommer åt ett flyktigt fält kommer att läsa dess nuvarande värde innan du fortsätter, istället för att (potentiellt) använda ett cachat värde. (Det finns dock ingen garanti om den relativa ordningen av flyktiga läsningar och skrivningar med vanliga läsningar och skrivningar, vilket innebär att det i allmänhet inte är en användbar trådkonstruktion.)
  • I Java 5 eller senare etablerar flyktiga läsningar och skrivningar en händer-före-relation , ungefär som att skaffa och släppa en mutex.

Att använda volatile kan vara snabbare än ett lås , men det kommer inte att fungera i vissa situationer före Java 5. Omfånget av situationer där volatile är effektivt utökades i Java 5; i synnerhet fungerar dubbelkontrollerad låsning nu korrekt.

I C#

I C# säkerställer volatile att kod som kommer åt fältet inte är föremål för vissa trådosäkra optimeringar som kan utföras av kompilatorn, CLR eller av hårdvara . När ett fält är markerat som flyktigt , instrueras kompilatorn att generera en "minnesbarriär" eller "stängsel" runt det, vilket förhindrar omordning av instruktioner eller cachning kopplat till fältet. När du läser ett flyktigt fält, genererar kompilatorn ett förvärv-fence , som förhindrar att andra läsningar och skrivningar till fältet, inklusive de i andra trådar, flyttas före stängslet. När du skriver till ett flyktigt fält genererar kompilatorn ett frigöringsfence ; detta stängsel förhindrar att andra läs- och skrivenheter till fältet flyttas efter stängslet.

Endast följande typer kan markeras som flyktiga : alla referenstyper, Single , Boolean , Byte , SByte , Int16 , UInt16 , Int32 , UInt32 , Char , och alla uppräknade typer med en underliggande typ av Byte , SByte , Int16 , UInt16 , UInt16 eller UInt32 . (Detta exkluderar värdestrukturer , såväl som de primitiva typerna Double , Int64 , UInt64 och Decimal .)

Att använda nyckelordet volatile stöder inte fält som skickas med referens eller infångade lokala variabler ; i dessa fall måste Thread.VolatileRead och Thread.VolatileWrite användas istället.

I själva verket inaktiverar dessa metoder vissa optimeringar som vanligtvis utförs av C#-kompilatorn, JIT-kompilatorn eller själva CPU:n. De garantier som tillhandahålls av Thread.VolatileRead och Thread.VolatileWrite är en uppsättning av garantierna som tillhandahålls av det flyktiga nyckelordet: istället för att generera ett "halvt fence" (dvs. ett förvärv-fence förhindrar endast omordning av instruktioner och cachning som kommer före det), VolatileRead och VolatileWrite genererar ett "helt staket" som förhindrar omordning av instruktioner och cachning av det fältet i båda riktningarna. Dessa metoder fungerar enligt följande:

  • Thread.VolatileWrite - metoden tvingar värdet i fältet att skrivas till vid anropspunkten. Dessutom måste eventuella tidigare programorderladdningar och -lagringar ske före anropet till VolatileWrite och eventuella senare programorderladdningar och -lagringar måste ske efter anropet.
  • Thread.VolatileRead - metoden tvingar värdet i fältet att läsas från vid anropspunkten. Dessutom måste eventuella tidigare programorderladdningar och -lagringar ske före anropet till VolatileRead och eventuella senare programorderladdningar och -lagringar måste ske efter anropet.

Thread.VolatileRead och Thread.VolatileWrite genererar ett helt staket genom att anropa metoden Thread.MemoryBarrier , som konstruerar en minnesbarriär som fungerar i båda riktningarna. Utöver motiven för att använda ett helt stängsel som ges ovan, är ett potentiellt problem med det flyktiga nyckelordet som löses genom att använda ett helt stängsel som genereras av Thread.MemoryBarrier : på grund av den asymmetriska naturen hos halvstängsel, är ett flyktigt fält med en skrivinstruktion följt av en läsinstruktion kan fortfarande ha exekveringsordern utbytt av kompilatorn. Eftersom hela staket är symmetriska är detta inte ett problem när du använder Thread.MemoryBarrier .

I Fortran

VOLATILE är en del av Fortran 2003- standarden, även om tidigare version stödde det som en tillägg. Att göra alla variabler flyktiga i en funktion är också användbart för att hitta aliasrelaterade buggar.

    
   
     heltal  ,  flyktigt  ::  i  ! När de inte definieras som flyktiga är följande två rader kod identiska   skriv  (  *  ,  *  )  i  **  2  ! Laddar variabeln i en gång från minnet och multiplicerar det värdet gånger sig själv   skriv  (  *  ,  *  )  i  *  i  ! Laddar variabeln i två gånger från minnet och multiplicerar dessa värden  

Genom att alltid "borra ner" till minnet av en VOLATILE, förhindras Fortran-kompilatorn från att omordna läsningar eller skrivningar till flyktiga ämnen. Detta synliggör för andra trådar åtgärder gjorda i denna tråd, och vice versa.

Användning av VOLATILE minskar och kan till och med förhindra optimering.

externa länkar