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
ochlongjmp
- 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.
Monteringsjämförelse | |
---|---|
Utan flyktigt nyckelord |
Med flyktigt sökord |
0 0
0
# include <stdio.h> int main () { /* Dessa variabler kommer aldrig att skapas på stack*/ int a = 10 , b = 100 , c = , d = ; /* "printf" kommer att anropas med argumenten "%d" och 110 (kompilatorn beräknar summan av a+b), alltså ingen overhead för att utföra addition vid körning */ printf ( "%d" , a + b ); /* Den här koden kommer att tas bort via optimering, men effekten av att 'c' och 'd' blir 100 kan ses när du anropar "printf" */ a = b ; c = b ; d = b ; /* Kompilatorn genererar kod där printf anropas med argumenten "%d" och 200 */ printf ( "%d" , c + d ) ; återvända ; }
|
0 0
0
# include <stdio.h> int main () { volatile int a = 10 , b = 100 , c = , d = ; printf ( "%d" , a + b ); a = b ; c = b ; d = b ; printf ( "%d" , c + d ); återvända ; }
|
gcc -S -O3 -masm=intel noVolatileVar.c -o utan.s | gcc -S -O3 -masm=intel VolatileVar.c -o with.s |
.fil "noVolatileVar.c" .intel_syntax noprefix .section .rodata.str1.1 , "aMS" , @progbits , 1 .LC0: .string "%d" .section .text.startup , "ax" , @progbits . p2align 4 ,, 15 .globl main .type main , @function main: .LFB11: .cfi_startproc sub rsp , 8 .cfi_def_cfa_offset 16 mov esi , 110 mov edi , OFFSET FLAT :. LC0 xor eax , eax call printf mov esi , 200 mov edi , OFFSET FLAT :. LC0 xor eax , eax call printf xor eax , eax add rsp , 8 .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE11: .size main . -main .ident "GCC: (GNU) 4.8.2" .note . stack , "" , @progbits
|
0
0
.fil "VolatileVar.c" .intel_syntax noprefix .section .rodata.str1.1 , "aMS" , @progbits , 1 .LC0: .string "%d" .section .text.startup , "ax" , @progbits . p2align 4 ,, 15 .globl main .type main , @function main: .LFB11: .cfi_startproc sub rsp , 24 .cfi_def_cfa_offset 32 mov edi , OFFSET FLAT :. LC0 mov DWORD PTR [ rsp ], 10 mov DWORD PTR [ rsp + 4 ], 100 mov DWORD PTR [ rsp + 8 ], mov DWORD PTR [ rsp + 12 ], mov esi , DWORD PTR [ rsp ] mov eax , DWORD PTR . [ rsp + 4 ] lägg till esi , eax xor eax , eax call printf mov eax , DWORD PTR [ rsp + 4 ] mov edi , OFFSET FLAT :. LC0 mov DWORD PTR [ rsp ] , eax mov eax , DWORD PTR [ rsp + 4 ] mov DWORD PTR [ rsp + 8 ] , eax mov eax , DWORD PTR [ rsp + 4 ] mov DWORD PTR [ rsp + 12 ] esi , DWORD PTR [ rsp + 8 ] mov eax , DWORD PTR [ rsp + 12 ] add esi , eax xor eax , eax call printf xor eax , eax add rsp , 24 .cfi_def_cfa_offset .cfi_1 mainpro .cfi 1 _ _ _ .-main .ident "GCC: (GNU) 4.8.2" .sektion .note.GNU-stack , "" , @progbits
|
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 tillVolatileWrite
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 tillVolatileRead
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.