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.
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.
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.
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.
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.
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.
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: danglingA 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.
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.
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);
}Í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?
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>?
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.
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.
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.
#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.
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.
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.