C++
C ++ 11 minnesmodell
Sök…
Anmärkningar
Olika trådar som försöker få åtkomst till samma minnesplats deltar i ett datarace om åtminstone en av operationerna är en modifiering (även känd som butikoperation ). Dessa dataraser orsakar odefinierat beteende . För att undvika dem måste man förhindra att dessa trådar samtidigt utför sådana motstridiga operationer.
Synkroniseringsprimitiv (mutex, kritisk sektion och liknande) kan skydda sådana åtkomster. Den minnesmodell som introducerades i C ++ 11 definierar två nya bärbara sätt att synkronisera åtkomst till minne i multigängad miljö: atomoperationer och staket.
Atomoperationer
Det är nu möjligt att läsa och skriva till en given minnesplats med hjälp av atombelastning och atomlagringsoperationer . För enkelhets skull lindas dessa in i std::atomic<t>
mallklassen. Denna klass slår in ett värde av typ t
men den här gången laddas och lagras till objektet är atomiska.
Mallen är inte tillgänglig för alla typer. Vilka typer som är tillgängliga är implementeringsspecifika, men det inkluderar vanligtvis de flesta (eller alla) tillgängliga integrerade typer samt pekartyper. Så att std::atomic<unsigned>
och std::atomic<std::vector<foo> *>
borde vara tillgängliga, medan std::atomic<std::pair<bool,char>>
förmodligen inte kommer att vara det.
Atomoperationer har följande egenskaper:
- Alla atomoperationer kan utföras samtidigt från flera trådar utan att orsaka odefinierat beteende.
- En atombelastning ser antingen det initiala värdet som atomobjektet konstruerades med eller värdet skrivet till det via någon atomlagringsoperation .
- Atomlagrar till samma atomobjekt beställs samma i alla trådar. Om en tråd redan har sett värdet på någon atomlagringsoperation , kommer efterföljande atombelastningsoperationer att se antingen samma värde eller värdet som lagras av efterföljande atomlagringsoperation .
- Atomläsning-modifiera-skrivoperationer gör att atombelastning och atomlagring kan hända utan någon annan atomlagring däremellan. Till exempel kan man atomiskt öka en räknare från flera trådar, och inget steg kommer att gå förlorat oavsett striden mellan trådarna.
- Atomoperationer får en valfri
std::memory_order
parameter som definierar vilka ytterligare egenskaper operationen har för andra minnesplatser.
std :: memory_order | Menande |
---|---|
std::memory_order_relaxed | inga ytterligare begränsningar |
std::memory_order_release → std::memory_order_acquire | om load-acquire ser värdet som lagras av store-release lagras sekvenserade innan store-release sker innan laster sekvenseras efter lasten får |
std::memory_order_consume | som memory_order_acquire men endast för beroende laster |
std::memory_order_acq_rel | kombinerar load-acquire och store-release |
std::memory_order_seq_cst | sekventiell konsistens |
Dessa minnesorder taggar tillåter tre olika minnesbeställningsdiscipliner: sekventiell konsistens , avslappnad och frigörande-förvärva med sin syskon release release .
Sekventiell konsekvens
Om ingen minnesordning är specificerad för en atomoperation, är beställningen standardvärde för sekventiell konsistens . Det här läget kan också väljas uttryckligen genom att tagga operationen med std::memory_order_seq_cst
.
Med denna ordning kan ingen minnesoperation korsa atomoperationen. Alla minnesoperationer sekvenserade innan atomoperationen sker före atomoperationen och atomoperationen sker före alla minnesoperationer som sekvenseras efter den. Det här läget är förmodligen det enklaste att resonera om, men det leder också till den största straffen för prestanda. Det förhindrar också alla kompilatoroptimeringar som annars kan försöka ordna operationer förbi atomoperationen.
Avslappnad beställning
Motsatsen till sekventiell konsistens är den avslappnade minnesbeställningen. Den väljs med taggen std::memory_order_relaxed
. Avslappnad atomdrift innebär inga begränsningar för andra minnesoperationer. Den enda effekten som återstår är att operationen i sig är fortfarande atomisk.
Release-Acquire Ordering
En atombutiksdrift kan märkas med std::memory_order_release
och en atom inläsningen kan märkas med std::memory_order_acquire
. Den första operationen kallas (atom) butik-frisläppning medan den andra kallas (atom) belastningsförvärv .
När lastförvärv ser värdet skrivet av en butiksläppning händer följande: alla butiksåtgärder sekvenserade innan butiksläppningen blir synliga för ( händer före ) lastoperationer som sekvenseras efter lastuppsamlingen .
Atomiska läs-modifiera-skrivoperationer kan också ta emot den kumulativa taggen std::memory_order_acq_rel
. Detta gör atomlastdelen av operationen ett atomlast förvärvar medan atom butiken delen blir atom lagra-frisättning.
Kompilatorn får inte flytta lagringsoperationer efter en atomutsläppsoperation . Det är inte heller tillåtet att flytta belastningsoperationer innan atombelastning (eller lastförbrukning ).
Observera också att det inte finns någon atombelastning eller atomförvaring . Att försöka skapa sådana operationer gör dem avslappnade operationer.
Release-Consume Ordering
Denna kombination är liknar frisättnings förvärvar, men denna gång atom belastningen är märkt med std::memory_order_consume
och blir (atom) last konsumerar drift. Detta läge är detsamma som frisläppningsförvärv med den enda skillnaden att bland de lastoperationer som sekvenseras efter lastkonsumtionen beställs endast dessa beroende på värdet som laddas av lastkonsumtionen .
stängsel
Staket gör det också möjligt att beställa minnesoperationer mellan trådar. Ett staket är antingen ett frigöringsstak eller köper staket.
Om ett frigöringsstaket inträffar före ett förvärvstaket, så är butiker som är sekvenserade innan frigöringsstaketet synliga för belastningar sekvenserade efter förvärvstaketet. För att garantera att frigöringsstaketet sker innan förvärvstaketet kan man använda andra synkroniseringsprimitiv inklusive avslappnade atomoperationer.
Behov av minnesmodell
int x, y;
bool ready = false;
void init()
{
x = 2;
y = 3;
ready = true;
}
void use()
{
if (ready)
std::cout << x + y;
}
En tråd kallar init()
-funktionen medan en annan tråd (eller signalhanterare) kallar funktionen use()
. Man kan förvänta sig att use()
-funktionen antingen kommer att skriva ut 5
eller göra ingenting. Detta kanske inte alltid är fallet av flera skäl:
CPU kan ordna om skrivningarna som händer i
init()
så att koden som verkligen körs kan se ut:void init() { ready = true; x = 2; y = 3; }
CPU kan ordna om läsningarna som händer vid
use()
så att den faktiskt exekverade koden kan bli:void use() { int local_x = x; int local_y = y; if (ready) std::cout << local_x + local_y; }
En optimerande C ++ -kompilerare kan besluta att ordna programmet på liknande sätt.
Sådan omarrangering kan inte ändra beteendet hos ett program som körs i en enda tråd eftersom en tråd inte kan lägga samman samtal till init()
och use()
. Å andra sidan i en multi-gängad inställning kan en tråd se en del av skrivningarna som utförs av den andra tråden där det kan hända att use()
kan se ready==true
och sopor i x
eller y
eller båda.
C ++ -minnesmodellen tillåter programmeraren att specificera vilka omordningsoperationer som är tillåtna och vilka inte är, så att ett flertrådat program också skulle kunna bete sig som förväntat. Exemplet ovan kan skrivas om på tråd-säkert sätt så här:
int x, y;
std::atomic<bool> ready{false};
void init()
{
x = 2;
y = 3;
ready.store(true, std::memory_order_release);
}
void use()
{
if (ready.load(std::memory_order_acquire))
std::cout << x + y;
}
Här utför init()
atomförvaringsfrisläpp . Detta lagrar inte bara värdet true
till ready
, utan berättar också för kompilatorn att den inte kan flytta den här operationen innan skrivoperationer som sekvenseras före den.
Funktionen use()
gör en atombelastningsoperation . Den läser det aktuella värdet för ready
och förbjuder även kompilatorn att placera läsoperationer som är sekvenserade efter det att hända innan atombelastningen .
Dessa atomoperationer får också kompilatorn att lägga till de hårdvaruinstruktioner som behövs för att informera CPU: n att avstå från oönskade omordningar.
Eftersom den atomiska lagringsfrisläppningen är på samma minnesplats som den atomiska belastningsförvärvningen , föreskriver minnesmodellen att om belastningsförvärvningsoperationen ser värdet skrivet av lagringsutsläppsoperationen , alla skrivningar utförs av init()
' s tråd före den butiksläppningen kommer att vara synlig för massor som use()
's trådkörning efter dess lastupptagning . Det är om use()
ser ready==true
, det är garanterat att se x==2
och y==3
.
Observera att kompilatorn och CPU-enheten fortfarande får skriva till y
innan de skriver till x
, och på liknande sätt kan läsningarna från dessa variabler som use()
ske i valfri ordning.
Staket exempel
Exemplet ovan kan också implementeras med stängsel och avslappnade atomoperationer:
int x, y;
std::atomic<bool> ready{false};
void init()
{
x = 2;
y = 3;
atomic_thread_fence(std::memory_order_release);
ready.store(true, std::memory_order_relaxed);
}
void use()
{
if (ready.load(std::memory_order_relaxed))
{
atomic_thread_fence(std::memory_order_acquire);
std::cout << x + y;
}
}
Om atombelastningen ser värdet som skrivits av atomlagret så sker butiken före lasten, och det gör också stängslarna: frigöringsstaketet händer innan förvärvstaketet gör skrivningarna till x
och y
som föregår frigöringsstaketet för att bli synliga till std::cout
uttalandet som följer förvärvstaketet.
Ett staket kan vara fördelaktigt om det kan minska det totala antalet anskaffnings-, frisläppnings- eller andra synkroniseringsoperationer. Till exempel:
void block_and_use()
{
while (!ready.load(std::memory_order_relaxed))
;
atomic_thread_fence(std::memory_order_acquire);
std::cout << x + y;
}
block_and_use()
snurrar tills den ready
flaggan är inställd med hjälp av avslappnad atombelastning. Sedan används ett enda förvärvstaket för att ge den minnesbeställning som krävs.