34.3 C++ atomics, atomic_ref és publication

A C atomics fejezetben (C atomics és memory order (15.4)) rögzítettük a _Atomic típusok, a memory_order és a happens-before alapjait. Itt ugyanazt a szerződést visszük át a C++ típusos std::atomic API-ra, de két új kérdéssel együtt: az atomikus objektum saját élettartamával és az általa publikált erőforrás élettartamával. Az atomikus művelet ugyanis csak az adott objektumhoz való hozzáférést teszi versenymentessé; nem tartja életben a mögötte lévő objektumot, és nem teszi automatikusan szálbiztossá a pointee állapotát. A fejezet C++20-at feltételez; a lock-free és a wait/notify konkrét implementációja target-, ABI- és standard library-függő, ezért a laborban ellenőrizni kell.

34.3.1 Az atomikus művelet és a lifetime szerződése

Atomikus műveletről akkor beszélünk, ha egy objektumhoz több szálon történő hozzáférés esetén a művelet oszthatatlannak látszik, és a nyelvi memóriamodell szerint nem keletkezik adatverseny. Ez nem azt jelenti, hogy a művelet lock-free, wait-free vagy gyors: a std::atomic<T> implementálható belső zárral is. Azt sem jelenti, hogy a T típusú objektum minden mezője atomikusan elérhető; a std::atomic<T> egy külön atomikus objektum, amelynek mérete és igazítása eltérhet a T-étől.

A std::atomic<T> primary templateje olyan T-vel példányosítható, amely trivially copyable, és amely megfelel a másolhatósági követelményeknek. Integrál, pointer és lebegőpontos T esetén specializációk adnak fetch_add, fetch_sub, illetve integrál esetén bitműveleti RMW-ket is. Nem trivially copyable típusra, például std::string-re a std::atomic<std::string> nem használható. A std::atomic<T> nem másolható és nem mozgatható; a szálak közötti átadása referencián vagy pointeren keresztül történik, és az atomikus objektumnak is élnie kell addig, amíg bármely szál hozzáférhet.

A lock-free elérhetőség nem nyelvi garancia. A is_lock_free() futásidejű lekérdezés, a is_always_lock_free pedig fordítási idejű konstans; utóbbi azt mondja meg, hogy az adott atomic<T> specializáció minden esetben lock-free-e. Egy atomic<int> x86-64 System V ABI-n jellemzően lock-free, de ezt a szabvány nem garantálja, és nagyobb T esetén a implementáció zárat használhat. A std::atomic_flag az egyetlen olyan atomikus típus, amelynek lock-free voltát a szabvány garantálja.

Az atomikus objektum élettartama külön szerződés. Ha egy szál még hozzáfér egy std::atomic<T> objektumhoz, miközben egy másik szál elpusztítja, az nem definiált viselkedés. A szálak joinolása vagy más, egyértelmű élettartam-kapcsolat nélkül az atomikus zászló sem véd meg az objektum eltűnése ellen.

34.3.2 Load, store, RMW, CAS és az ordering

A std::atomic<T> alapműveletei a load, store, exchange, compare_exchange_weak, compare_exchange_strong, valamint a támogatott T esetén a fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor. A load és a store egyetlen atomikus olvasás, illetve írás. Az exchange atomikusan kicserél. A compare_exchange feltételes RMW: csak akkor ír, ha az aktuális érték megegyezik a várt értékkel; ellenkező esetben a várt értéket az aktuális értékre frissíti. A weak változat megengedhet spurius hibát, ezért loopban kell használni; a strong nem ad spurius hibát, de drágább lehet.

Az ordering nem dekoráció, hanem a láthatóság szerződése. A relaxed csak atomikusságot ad, szálak közötti rendezést nem. Az acquire load vagy RMW esetén azt garantálja, hogy a művelet után következő olvasások és írások nem rendeződnek el a load elé. A release store vagy RMW esetén azt garantálja, hogy a store előtti olvasások és írások nem rendeződnek el a store mögé. Az acq_rel csak RMW-n értelmes, és egyszerre ad acquire és release hatást. A seq_cst ezeken felül egyetlen teljes sorrendet ad az összes seq_cst műveletre; ez a default, de nem mindig szükséges.

Ordering Hol használható Mit ad
relaxed load, store, RMW csak atomikusság
acquire load, RMW a későbbi hozzáférések nem kerülnek elé
release store, RMW a korábbi hozzáférések nem kerülnek mögé
acq_rel RMW acquire + release
seq_cst mind teljes sorrend a seq_cst műveletekre

Érvénytelen ordering kombináció például a release load vagy a release failure ordering egy compare_exchange-ben; az ilyen előfeltétel megsértése nem definiált viselkedés. A failure ordering nem lehet erősebb a success orderingnél, és nem lehet release vagy acq_rel.

Relaxed számlálóra tipikus példa a statisztikai számlálás. A következő kódban a fetch_add atomikus, ezért nem vesznek el növelések, de a relaxed nem publikál más adatot:

#include <atomic>
#include <thread>
#include <iostream>

std::atomic<unsigned long> hits{0};

void worker() {
    for (int i = 0; i < 1000; ++i) {
        hits.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread a(worker);
    std::thread b(worker);
    a.join();
    b.join();
    std::cout << hits.load(std::memory_order_relaxed) << '\n';
}

A join() után a fő szál a join szinkronizációs hatása miatt látja a szálak befejeződését; a relaxed itt nem a join helyett működik, hanem a számláló növelésének atomikusságát adja. Ha a számláló mellett más, nem atomikus adatot is publikálnánk, a relaxed nem lenne elég.

A CAS-loop mintája jól mutatja a compare_exchange_weak használatát. Az expected értékét a művelet hiba esetén frissíti, ezért minden iterációban az aktuális értékkel kell dolgozni:

#include <atomic>

std::atomic<int> maximum{0};

void update_max(int candidate) {
    int old = maximum.load(std::memory_order_relaxed);
    while (candidate > old &&
           !maximum.compare_exchange_weak(
               old, candidate,
               std::memory_order_release,
               std::memory_order_relaxed)) {
        // old már az aktuális érték; újra próbáljuk
    }
}

Ha a CAS success orderingje release, akkor a candidate előtti írások a sikeres store-hoz kapcsolódnak; ha csak a maximum érték érdekes, relaxed is elég lehet. A weak spurius hibája miatt a loop kötelező; a strong használata nem menti meg a versenyt, csak a spurius hibát zárja ki.

34.3.3 Publication release/acquire szerződéssel

A leggyakoribb minta a publisher–consumer kapcsolat: az egyik szál előkészít egy adatot, majd egy atomikus jelzővel vagy pointerrel publikálja; a másik szál acquire-tel olvassa a jelzőt, és utána olvassa az adatot. A release store és az acquire load akkor szinkronizál, ha ugyanarra az atomikus objektumra történnek, és az acquire load a release store által írt értéket, illetve a release sequence megfelelő későbbi értékét látja. Ekkor a release store előtti írások happens-before kapcsolatba kerülnek az acquire load utáni olvasásokkal.

A levezetés lépései: a publisher a payload írását sequenced-before helyzetbe hozza a release store-hoz; a release store és a consumer acquire loadja között synchronizes-with él; a consumer acquire loadja sequenced-before a payload olvasásához; a három viszony tranzitivitásából a payload írása happens-before a payload olvasásához. Ezért nincs adatverseny, és a consumer a publikált értéket látja.

#include <atomic>
#include <cassert>
#include <thread>

std::atomic<bool> ready{false};
int payload = 0;

void publisher() {
    payload = 42;                                  // nem atomikus, de a store előtt van
    ready.store(true, std::memory_order_release);
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) {
        // várakozás
    }
    assert(payload == 42);
}

int main() {
    std::thread p(publisher);
    std::thread c(consumer);
    p.join();
    c.join();
}

Ha a ready.store relaxed, és a ready.load is relaxed, akkor a payload írása és olvasása között nincs happens-before; a két szál egy időben ér hozzá egy nem atomikus objektumhoz, ami adatverseny és nem definiált viselkedés. A seq_cst mindkét oldalon szintén helyes, de erősebb a szükségesnél. A release/acquire itt a minimális, indokolható választás.

Fontos feltétel, hogy a consumer valóban a publisher értékét lássa. Ha a load egy korábbi értéket olvas, abból az adott store-ra nézve nincs synchronizes-with. A while ciklus ezért addig olvas, amíg a publikált értéket meg nem látja. Több publisher esetén a release sequence és az RMW-k szerepe bonyolultabb; ilyenkor vagy seq_cst-t, vagy külön szinkronizációs protokollt használunk.

Ugyanez a minta pointerrel is működik:

struct Config {
    int timeout;
    int retries;
};

std::atomic<Config*> current{nullptr};

void publish(Config* c) {
    current.store(c, std::memory_order_release);
}

Config* snapshot() {
    return current.load(std::memory_order_acquire);
}

A publish előtt inicializált Config mezői a release store miatt válnak láthatóvá annak, aki a pointert acquire-tel olvassa. A pointer atomikussága azonban nem terjeszti ki a Config élettartamát; erről a következőkben külön szólunk.

34.3.4 atomic_ref: atomikus hozzáférés létező objektumhoz

A std::atomic_ref<T> C++20-as eszköz arra az esetre, amikor egy már létező T objektumot szeretnénk atomikusan elérni anélkül, hogy std::atomic<T> burkolóba tennénk. Ilyen lehet egy tömb eleme, egy C struct mezője, memóriába mapolt adat vagy egy olyan interfész, amely T&-et ad. Az atomic_ref nem tulajdonol, nem hoz létre új tárolót, és nem hosszabbítja meg a hivatkozott objektum élettartamát.

Az atomic_ref<T> követelményei: T trivially copyable; a hivatkozott objektum igazítása legalább std::atomic_ref<T>::required_alignment; a hivatkozott objektum élettartama hosszabb az atomic_ref élettartamánál; és amíg az atomic_ref él, a hivatkozott objektumot csak atomic_ref-en keresztül szabad elérni. Ez az utolsó pont az exkluzív hozzáférés szerződése, és szigorúbb az általános adatverseny-szabálynál: amíg bármely atomic_ref él, a nem atomikus hozzáférés akkor is tilos (nem definiált viselkedés), ha csak olvasás, és semmilyen írással nem ütközne. Az atomic_ref élettartamán kívül viszont a szokásos szabály érvényes: egymással nem ütköző, csak olvasó hozzáférések szinkronizáció nélkül is megengedettek. Több atomic_ref ugyanarra az objektumra megengedett.

// atomic_ref_cells.cpp — C++20: minden atomic_ref-cél elem igazítása garantált
#include <array>
#include <atomic>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <thread>

// Az igazítás az ELEMTÍPUSON van: így a tömb minden eleme required_alignment
// többszörös címen kezdődik, nem csak az első.
struct alignas(std::atomic_ref<int>::required_alignment) Cell {
    int value = 0;
};

std::array<Cell, 4> counters{};

void bump(std::size_t i) {
    std::atomic_ref<int> ref(counters[i].value);
    for (int n = 0; n < 1000; ++n) {
        ref.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    for (const Cell& c : counters) {
        assert(reinterpret_cast<std::uintptr_t>(&c.value)
               % std::atomic_ref<int>::required_alignment == 0);
    }
    std::thread a(bump, 0);
    std::thread b(bump, 0);
    std::thread c(bump, 1);
    a.join();
    b.join();
    c.join();
    // Itt már nem él atomic_ref, a nem atomikus olvasás megengedett.
    return counters[0].value == 2000 && counters[1].value == 1000 ? 0 : 1;
}

Az alignas(std::atomic_ref<int>::required_alignment) nem kozmetika: ha az objektum nincs a megkövetelt igazításra igazítva, az atomic_ref használata nem definiált viselkedés. A required_alignment nagyobb is lehet, mint alignof(int), ezért az igazítást az elemtípusra (Cell) kell tenni: ha csak a tömböt igazítanánk (alignas(...) std::array<int, 4>), az csak az első elemre garantálná a megkövetelt igazítást, a counters[1] már sizeof(int) eltolásra kerülne. A false sharing elkerülése ettől külön cél: ahhoz a cellákat cache-line méretre kellene igazítani, ami az atomic_ref-előfeltételtől független döntés.

Buildjegyzőkönyv (lab-gcc profil). g++ -std=c++20 -Wall -Wextra -Wpedantic -O2 -pthread atomic_ref_cells.cpp figyelmeztetés nélkül fordul, a program 0 kóddal fut (minden counters[i].value cím igazítását assert ellenőrzi), TSan alatt (setarch -R) is. A std::atomic_ref<int> lehet lock-free vagy sem; ezt is_lock_free, illetve is_always_lock_free mutatja. A fetch_add relaxed itt azért elég, mert csak a számláló értéke érdekes, és a szálak joinja után olvassuk.

Az atomic_ref élettartama különösen veszélyes, ha referencia vagy pointer formájában kikerül a lokális scope-ból. Ha az atomic_ref tovább él, mint a hivatkozott objektum, dangling hivatkozás lesz; az atomikus művelet ettől még nem lesz biztonságos.

34.3.5 C++20 wait/notify: érték és predikátum

A C++20 std::atomic<T>::wait és notify_one/notify_all egy könnyű várakozási mechanizmust ad. A wait(old, order) addig blokkol, amíg a rendszer értesítést nem ad, és az atomikus objektum értéke el nem tér a megadott old értéktől. A művelet nem spurius módon tér vissza értékváltozás nélkül, de a notify önmagában nem tárol eseményt: ha nincs várakozó szál, az értesítés elveszik. Ezért először ellenőrizni kell az értéket, majd szükség esetén wait-tel várakozni.

#include <atomic>
#include <cassert>
#include <thread>

std::atomic<int> slot{0};
int payload = 0;

void producer() {
    payload = 42;
    slot.store(1, std::memory_order_release);
    slot.notify_one();
}

void consumer() {
    int v = slot.load(std::memory_order_acquire);
    while (v == 0) {
        slot.wait(0, std::memory_order_acquire);
        v = slot.load(std::memory_order_acquire);
    }
    assert(payload == 42);
}

int main() {
    std::thread p(producer);
    std::thread c(consumer);
    p.join();
    c.join();
}

A wait az értéket ellenőrzi, nem tetszőleges predikátumot. Ha a várakozási feltétel összetett, például „a sor nem üres és nincs leállítás”, akkor a wait után újra kell értékelni a predikátumot, és csak akkor szabad továbbmenni, ha teljesül. A notify_all akkor indokolt, ha több várakozó szál közül több is továbbléphet; a notify_one egy várakozót ébreszt. A wait/notify nem helyettesít condition variable-t ott, ahol a várakozás feltétele egy nem atomikus adatszerkezetre vonatkozik; ilyenkor a mutex és a condition variable (RAII lockok, condition variable és wait predicate (34.2)) továbbra is a megfelelő eszköz.

34.3.6 Az atomikus pointer nem tartja életben a pointee-ot

A std::atomic<Config*> csak a pointer értékét védi. Ha a publisher release-store-ol egy pointert, majd a consumer acquire-loaddal megkapja és dereferálja, a Config objektumnak élnie kell a dereferálás teljes ideje alatt. Az atomikus művelet nem számol referenciát, nem foglal és nem tart életben semmit. A következő sorrend hibás, mert a publisher a consumer használata előtt felszabadíthatja az objektumot:

Config* c = new Config{30, 3};
current.store(c, std::memory_order_release);
// ...
delete c; // a consumer még dolgozhat a pointerrel: dangling

A helyes tulajdonosi protokoll legalább az egyik: a consumerek joinolása után szabadítunk fel; referencia-számlálást használunk; vagy epoch/hazard-pointer alapú visszanyerést alkalmazunk. A current.store(nullptr, release) önmagában nem oldja meg a versenyt, mert egy consumer a régi pointert már betölthette, és a delete után még használhatja. A std::shared_ptr referenciaszámlálója atomikus, de ez nem teszi a pointee mezőit automatikusan szálbiztossá, és a publication ordering itt is külön kérdés; a részletes visszanyerési minták a shared ownership, reclamation és konkurens review (34.6) témája.

Ugyanez vonatkozik az atomikus objektum saját élettartamára is. Ha egy std::atomic<bool> vagy std::atomic<int> lokális változó, és egy szál még hivatkozik rá, amikor a scope véget ér, a program nem definiált. A szálak joinolása, a std::jthread destruktorának stop/join viselkedése vagy egyéb, dokumentált élettartam-kapcsolat szükséges.

34.3.7 Gyakori hibák és javításuk

Az egyik leggyakoribb hiba a volatile használata atomikus helyett. A volatile nem ad szálak közötti szinkronizációt, nem garantál atomikusságot, és nem akadályozza meg az adatversenyt. Helyette std::atomic<T> vagy mutex kell.

A második: a publication relaxed jelzővel. A relaxed store/load csak az adott objektumra ad atomikusságot; a payload nem atomikus olvasása és írása között nem teremt happens-before-t. Ilyenkor a helyes megoldás release/acquire, vagy a teljes adat védelme lockkal.

A harmadik: a compare_exchange_weak használata loop nélkül. A spurius hiba nem hiba a programban, hanem a művelet természete; a loop és az expected frissítése kötelező. A strong sem teszi feleslegessé a versenyhelyzet kezelését, csak a spurius hibát zárja ki.

A negyedik: az atomic_ref nem megfelelő igazítása vagy nem atomikus hozzáféréssel való keverése. Az atomic_ref nem „varázsolja” atomikussá a mögöttes objektumot más hozzáférések számára; a szerződés az, hogy amíg él, minden hozzáférés atomic_ref-en keresztül történik. Az igazítást required_alignment-nel kell biztosítani.

Az ötödik: az atomikus pointerből azt feltételezni, hogy a pointee élettartama automatikusan biztonságos. Az atomikus pointer csak a cím értékét védi. A pointee felszabadítása, újrahasznosítása és a rá mutató régi pointerek érvényessége külön tulajdonosi protokollt igényel.

A hatodik: a wait/notify használata elveszett ébresztéssel. Ha a notify előbb történik, mint a wait, az értesítés elveszik; ezért a várakozás előtt ellenőrizni kell az értéket, és a wait után újra kell értékelni a feltételt. A wait nem univerzális „eseménysor”; a C++20 atomikus wait csak az értékváltozásra és az értesítésre épül.

34.3.8 Feladatok

34.3.8.1 1. feladat: ordering javítása

Adott a következő publikációs kód. A ready atomikus, a payload nem atomikus. Magyarázd meg, miért hibás a relaxed használat, majd javítsd minimális orderinggal. Add meg a szálak élettartamára vonatkozó feltételt is.

std::atomic<bool> ready{false};
int payload = 0;

void publisher() {
    payload = 7;
    ready.store(true, std::memory_order_relaxed);
}

void consumer() {
    while (!ready.load(std::memory_order_relaxed)) {}
    assert(payload == 7);
}

34.3.8.2 2. feladat: CAS-alapú maximum

Írj update_max(int) függvényt, amely std::atomic<int>-ban tartja a maximumot, és compare_exchange_weak-et használ. Indokold a success és failure ordering választását. Mi történik, ha a weak helyett strong-ot használunk, és miért nem lesz ettől versenymentes a frissítés?

34.3.8.3 3. feladat: atomic_ref tömbön

Adott egy std::array<int, 8> számlálótömb. Több szál ugyanazokat az elemeket növeli std::atomic_ref<int>-en keresztül. Írj kódot, amely biztosítja a szükséges igazítást, és magyarázd meg, miért nem szabad a szálak futása közben nem atomikusan olvasni a tömböt. Milyen feltétel mellett lock-free az atomic_ref<int>?

34.3.8.4 4. feladat: élettartam-diagnosztika

Adott egy std::atomic<Widget*> pointer, amelyet az egyik szál release-store-ol, a másik acquire-loadol és dereferál. A publisher a store után azonnal delete-eli a Widget-et. Azonosítsd a hibát, és adj két különböző, helyes tulajdonosi megoldást. Az egyik megoldás join-alapú, a másik referencia-számlálásos legyen; mindkettőnél írd le, ki a tulajdonos és meddig él a pointee.

34.3.9 Megoldások

34.3.9.1 1. megoldás

A relaxed store és load között nincs release/acquire kapcsolat, ezért a payload írása és olvasása nem kerül happens-before viszonyba. Két szál ér hozzá ugyanahhoz a nem atomikus objektumhoz legalább az egyik oldalon írással, ezért adatverseny keletkezik, ami nem definiált viselkedés. A payload értéke nem garantáltan 7 a consumerben.

A javítás:

void publisher() {
    payload = 7;
    ready.store(true, std::memory_order_release);
}

void consumer() {
    while (!ready.load(std::memory_order_acquire)) {}
    assert(payload == 7);
}

A ready objektumnak élnie kell addig, amíg bármelyik szál hozzáférhet; a példában ezt a join() biztosítja. A payload élettartama szintén legalább a consumer olvasásáig tart. Ellenőrzési feltétel: a consumer acquire loadja a publisher release storejától származó true értéket látja, és a payload olvasása a load után történik.

34.3.9.2 2. megoldás

void update_max(int candidate) {
    int old = maximum.load(std::memory_order_relaxed);
    while (candidate > old &&
           !maximum.compare_exchange_weak(
               old, candidate,
               std::memory_order_release,
               std::memory_order_relaxed)) {
    }
}

A success ordering release akkor indokolt, ha a candidate előtti írásokat is publikálni akarjuk; ha csak a maximum számérték kell, relaxed is elég. A failure ordering relaxed, mert hiba esetén nem írunk, és nem akarunk acquire-hatást; a failure nem lehet release vagy acq_rel. A weak spurius hibát adhat, ezért a loop újrapróbálkozik, és az old értéket a CAS hiba esetén frissíti. A strong csak a spurius hibát zárja ki; a két szál közötti verseny ettől még megmarad, ezért a CAS-loop továbbra is kell. Ellenőrzési feltétel: a végső maximum nem lehet kisebb egyetlen sikeresen beírt jelöltnél sem, és a CAS csak akkor ír, ha az aktuális érték még az old.

34.3.9.3 3. megoldás

#include <atomic>
#include <array>
#include <thread>
#include <cstddef>

alignas(std::atomic_ref<int>::required_alignment)
std::array<int, 8> counters{};

void bump(std::size_t i, int n) {
    std::atomic_ref<int> ref(counters[i]);
    for (int k = 0; k < n; ++k) {
        ref.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread a(bump, 0, 1000);
    std::thread b(bump, 0, 1000);
    a.join();
    b.join();
    return counters[0] == 2000 ? 0 : 1;
}

Az alignas(std::atomic_ref<int>::required_alignment) biztosítja, hogy a tömb elemei megfeleljenek az atomic_ref igazítási követelményének. Amíg a bump függvényben él az atomic_ref, a counters[i]-hez csak azon keresztül szabad hozzáférni; a mainben a join után már nem él atomic_ref, ezért a nem atomikus olvasás megengedett. Az atomic_ref<int> lock-free volta implementációfüggő; is_always_lock_free és is_lock_free() segítségével ellenőrizhető, de a szabvány nem garantálja. Ellenőrzési feltétel: a két szál 2000-re növeli a 0. elemet, és nincs nem atomikus hozzáférés a szálak futása közben.

34.3.9.4 4. megoldás

A hiba: a publisher release-store után azonnal delete-eli a Widget-et, miközben a consumer acquire-loaddal megkaphatja a pointert, és a delete után dereferálhatja. Az atomikus pointer csak a pointer értékét védi; a Widget élettartamát nem. A consumer számára a pointer dangling lehet, a dereferálás nem definiált viselkedés.

Join-alapú megoldás: a Widget tulajdonosa a publisher vagy egy külön owner objektum. A publisher a store után nem törölhet, amíg a consumerek be nem fejeződtek. A consumerek joinolása után szabad a delete, majd a pointert nullára állítani. Itt a tulajdonos az owner objektum, a pointee élettartama a consumerek joinjáig tart. Ellenőrzési feltétel: nincs olyan consumer, amely a delete után töltené be vagy használná a pointert.

Referencia-számlálásos megoldás: a Widget-et std::shared_ptr<Widget> tartja életben; a consumer a pointer betöltésekor vagy egy rövid kritikus szakaszban megnöveli a referenciaszámot, és a használat végén csökkenti. A publisher az utolsó referencia elengedésekor szabadít fel. Itt a tulajdonos a referenciaszámláló által meghatározott utolsó birtokos, a pointee élettartama az utolsó shared_ptr megszűnéséig tart. Fontos: a shared_ptr referenciaszámlálója atomikus, de a Widget mezői nem válnak automatikusan szálbiztossá; a publikálást release/acquire vagy lock védi. A részletes reclamation-minták a shared ownership, reclamation és konkurens review (34.6) témájában következnek.

34.3.10 Összefoglaló

A std::atomic<T> típusos, de nem varázseszköz: atomikusságot ad a saját objektumára, nem feltétlenül lock-free, és nem terjeszti ki sem a saját, sem a pointee élettartamát. A load, store, RMW és CAS műveletekhez tudatosan kell orderingot választani: relaxed csak számláláshoz, release/acquire publicationhöz, seq_cst pedig akkor, ha a teljes sorrend indokolt. Az atomic_ref<T> létező objektumot tesz atomikusan elérhetővé, de csak megfelelő igazítás, élő háttérobjektum és exkluzív hozzáférés mellett. A C++20 wait/notify értékváltozásra várakozik; elveszett ébresztés és összetett predikátum esetén ciklusos újraellenőrzés kell. Az atomikus pointer publikálása mindig tulajdonosi protokollal egészül ki: join, referencia-számlálás vagy epoch-alapú visszanyerés nélkül a pointer érvényes lehet, a pointee már nem.