Zoeken…


Opmerkingen

Verschillende threads die toegang proberen te krijgen tot dezelfde geheugenlocatie nemen deel aan een gegevensrace als ten minste een van de bewerkingen een wijziging is (ook bekend als winkelbewerking ). Deze gegevensraces veroorzaken ongedefinieerd gedrag . Om ze te vermijden, moet men voorkomen dat deze threads gelijktijdig dergelijke conflicterende bewerkingen uitvoeren.

Synchronisatieprimitieven (mutex, kritieke sectie en dergelijke) kunnen dergelijke toegangen bewaken. Het geheugenmodel geïntroduceerd in C ++ 11 definieert twee nieuwe draagbare manieren om de toegang tot geheugen te synchroniseren in een multi-threaded omgeving: atomaire operaties en hekken.

Atoomoperaties

Het is nu mogelijk om te lezen en te schrijven naar een gegeven geheugenlocatie door het gebruik van atomaire belasting en atomaire bewerkingen. Voor het gemak zijn deze verpakt in de sjabloonklasse std::atomic<t> . Deze klasse heeft een waarde van type t maar deze keer wordt het object geladen en opgeslagen in atomaire volgorde.

De sjabloon is niet voor alle typen beschikbaar. Welke types beschikbaar zijn, is implementatiespecifiek, maar dit omvat meestal de meeste (of alle) beschikbare integrale types evenals pointer-types. Zodat std::atomic<unsigned> en std::atomic<std::vector<foo> *> beschikbaar moet zijn, terwijl std::atomic<std::pair<bool,char>> waarschijnlijk niet zal zijn.

Atoombewerkingen hebben de volgende eigenschappen:

  • Alle atomaire bewerkingen kunnen gelijktijdig worden uitgevoerd vanuit meerdere threads zonder ongedefinieerd gedrag te veroorzaken.
  • Een atomaire belasting zal de initiële waarde zien waarmee het atomaire object is geconstrueerd, of de waarde die ernaar is geschreven via een atomaire-opslagbewerking .
  • Atoomopslagplaatsen voor hetzelfde atomaire object worden in alle threads hetzelfde geordend. Als een thread de waarde van een atomaire-opslagbewerking al heeft gezien, zien volgende atomaire-laadbewerkingen dezelfde waarde of de waarde die is opgeslagen door de volgende atomaire-opslagbewerking .
  • Atomaire lees-wijzig-schrijfbewerkingen zorgen ervoor dat atomaire belasting en atomaire opslag plaatsvinden zonder dat er een andere atomaire opslag tussen zit. Men kan bijvoorbeeld een teller atomair ophogen uit meerdere threads en er zal geen increment verloren gaan ongeacht de discussie tussen de threads.
  • Atomaire bewerkingen ontvangen een optionele parameter std::memory_order die definieert welke aanvullende eigenschappen de bewerking heeft met betrekking tot andere geheugenlocaties.
std :: memory_order Betekenis
std::memory_order_relaxed geen aanvullende beperkingen
std::memory_order_releasestd::memory_order_acquire als load-acquire de waarde ziet die is opgeslagen door store-release , worden de sequenties van de winkels opgeslagen voordat de store-release plaatsvindt voordat de sequenties worden bepaald nadat de load
std::memory_order_consume zoals memory_order_acquire maar alleen voor afhankelijke belastingen
std::memory_order_acq_rel combineert load-acquire en store-release
std::memory_order_seq_cst opeenvolgende consistentie

Deze geheugenbesteltags staan drie verschillende geheugenvolgordisciplines toe: opeenvolgende consistentie , ontspannen en release-acquireren met zijn broer- release release-consumeren .

Sequentiële consistentie

Als er geen geheugenvolgorde is opgegeven voor een atomaire bewerking, wordt de volgorde standaard ingesteld op sequentiële consistentie . Deze modus kan ook expliciet worden geselecteerd door de bewerking te taggen met std::memory_order_seq_cst .

Met deze volgorde kan geen enkele geheugenbewerking de atomaire bewerking overschrijden. Alle geheugenbewerkingen die zijn gesequenced voordat de atomaire bewerking plaatsvindt vóór de atomaire bewerking en de atomaire bewerking vindt plaats voordat alle geheugenbewerkingen die daarna worden gesequenced. Deze modus is waarschijnlijk de gemakkelijkste om over te redeneren, maar het leidt ook tot de grootste straf voor de prestaties. Het voorkomt ook alle compileroptimalisaties die anders proberen bewerkingen opnieuw te rangschikken voorbij de atomaire bewerking.

Ontspannen bestelling

Het tegenovergestelde van sequentiële consistentie is de ontspannen ordening van het geheugen. Het wordt geselecteerd met de tag std::memory_order_relaxed . Ontspannen atomaire werking legt geen beperkingen op voor andere geheugenbewerkingen. Het enige effect dat overblijft, is dat de operatie zelf nog steeds atomair is.

Bestelling vrijgeven

Een atomaire-opslagbewerking kan worden getagd met std::memory_order_release en een atomaire-laadbewerking kan worden getagd met std::memory_order_acquire . De eerste bewerking wordt (atomaire) store-release genoemd, terwijl de tweede bewerking (atomaire) load-acquise wordt genoemd .

Wanneer load-acquire de waarde ziet die is geschreven door een store-release, gebeurt het volgende: alle store-operaties die zijn gesequenced voordat de store-release zichtbaar wordt voor ( gebeuren voor ) load-operaties die worden gesequenced na de load-acquisitie .

Atomaire lees-wijzig-schrijf-bewerkingen kunnen ook de cumulatieve tag std::memory_order_acq_rel . Dit maakt de atomaire belastingsgedeelte van de operatie een atomaire load-verwerven terwijl de atomaire geheugendeel wordt atomaire opslag afgifte.

Het is de compiler niet toegestaan om winkelbewerkingen te verplaatsen na een atomaire vrijgavebewerking . Het is ook niet toegestaan om laadbewerkingen te verplaatsen voordat atomaire belasting wordt verkregen (of belasting wordt verbruikt ).

Merk ook op dat er geen atomaire belastingvrijgave of atomaire opslagverwerving is . Als u dergelijke bewerkingen probeert uit te voeren, worden ze ontspannen .

Bestelling vrijgeven

Deze combinatie is vergelijkbaar met release-acquireren , maar deze keer is de atomaire belasting getagd met std::memory_order_consume en wordt deze (atomaire) load-consumptiebewerking . Deze modus is hetzelfde als release-acquireren met het enige verschil dat onder de laadbewerkingen waarvan de sequentie is bepaald na het load-consumeren alleen deze worden besteld, afhankelijk van de waarde die wordt geladen door het load-consumeren .

Fences

Met hekken kunnen geheugenbewerkingen ook tussen threads worden geordend. Een hek is een vrijgavehek of verkrijgt een hek.

Als een vrijgavehek plaatsvindt vóór een verkrijgingshek, dan worden de opeenvolgende winkels opgeslagen voordat het vrijgavehek zichtbaar is voor de opeenvolgingen van lasten na het verkrijgingshek. Om te garanderen dat de vrijgaveomheining plaatsvindt vóór de verkrijgingsomheining, kan men andere synchronisatieprimitieven gebruiken, waaronder ontspannen atomaire bewerkingen.

Behoefte aan geheugenmodel

int x, y;
bool ready = false;

void init()
{
  x = 2;
  y = 3;
  ready = true;
}
void use()
{
  if (ready)
    std::cout << x + y;
}

Eén thread roept de functie init() , terwijl een andere thread (of signaalhandler) de functie use() aanroept. Je zou verwachten dat de functie use() 5 afdrukt of niets doet. Dit kan om verschillende redenen niet altijd het geval zijn:

  • De CPU kan de schrijfbewerkingen die in init() plaatsvinden opnieuw rangschikken init() zodat de code die daadwerkelijk wordt uitgevoerd er als volgt uitziet:

    void init()
    {
      ready = true;
      x = 2;
      y = 3;
    }
    
  • De CPU kan de reads die in use() gebeuren opnieuw ordenen use() zodat de daadwerkelijk uitgevoerde code kan worden:

    void use()
    {
      int local_x = x;
      int local_y = y;
      if (ready)
        std::cout << local_x + local_y;
    }
    
  • Een optimaliserende C ++ compiler kan besluiten het programma op dezelfde manier opnieuw te ordenen.

Zo'n herschikking kan het gedrag van een programma dat in één thread draait niet veranderen, omdat een thread de aanroepen van init() en use() niet kan verscherven. Aan de andere kant kan in een multi-threaded instelling een thread een deel van de schrijfopdrachten zien die door de andere thread worden uitgevoerd, waar het kan gebeuren dat use() ready==true en rotzooi in x of y of beide.

Met het C ++ -geheugenmodel kan de programmeur specificeren welke herschikkingshandelingen zijn toegestaan en welke niet, zodat een multi-threaded programma zich ook zou kunnen gedragen zoals verwacht. Het bovenstaande voorbeeld kan op deze manier als volgt worden herschreven:

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;
}

Hier voert init() bewerking voor atomaire opslagvrijgave uit. Dit slaat niet alleen de waarde true in ready , maar vertelt de compiler ook dat deze deze bewerking niet kan verplaatsen voordat schrijfbewerkingen worden uitgevoerd die ervoor zijn gesequenced .

De functie use() voert een bewerking voor het verkrijgen van atomaire belasting uit. Het leest de huidige waarde van ready en verbiedt ook de compiler leesbewerkingen te plaatsen waarvan de volgorde wordt bepaald voordat het gebeurt voordat de atoombelasting wordt verworven .

Deze atomaire bewerkingen zorgen er ook voor dat de compiler alle hardware-instructies plaatst die nodig zijn om de CPU te informeren af te zien van ongewenste nabestellingen.

Omdat de atomaire store-release zich op dezelfde geheugenlocatie bevindt als de atomaire load-acquisitie , bepaalt het geheugenmodel dat als de load-acquisitiebewerking de waarde ziet die is geschreven door de store-releasebewerking , alle schrijfbewerkingen worden uitgevoerd door init() ' s thread voorafgaand aan die store-release zal zichtbaar zijn voor ladingen die de thread van use() na het laden van de lading . Dat is als use() ready==true ziet, dan is het gegarandeerd x==2 en y==3 .

Merk op dat de compiler en de CPU nog steeds naar y schrijven voordat ze naar x schrijven, en op dezelfde manier kunnen de reads van deze variabelen in use() in elke volgorde gebeuren.

Hek voorbeeld

Het bovenstaande voorbeeld kan ook worden geïmplementeerd met hekken en ontspannen atomaire bewerkingen:

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;
  }
}

Als de atoomlastbewerking de waarde ziet die is geschreven door de atoomopslag, gebeurt de opslag vóór de belasting, en de hekwerken ook: de vrijgaveomheining gebeurt voordat de acquisitieomheining het schrijven naar x en y dat voorafgaat aan de vrijgaveomheining zichtbaar maakt naar de std::cout instructie die volgt op het hek om te verwerven.

Een afrastering kan nuttig zijn als het het totale aantal acquisitie-, release- of andere synchronisatiebewerkingen kan verminderen. Bijvoorbeeld:

void block_and_use()
{
  while (!ready.load(std::memory_order_relaxed))
    ;
  atomic_thread_fence(std::memory_order_acquire);
  std::cout << x + y;
}

De functie block_and_use() draait totdat de vlag ready wordt ingesteld met behulp van een ontspannen block_and_use() . Vervolgens wordt een enkele acquisitiehek gebruikt om de benodigde geheugenvolgorde te bieden.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow