Atomové operace a uspořádání paměti
Pochopte programování bez zámku pomocí std::atomic. Zvládněte uspořádání paměti (uvolněná, nabytá, uvolněná, seq_cst), implementujte spinlock s atomic_flag a naučte se vzor porovnat a vyměnit (CAS).
Co jsou atomové operace?
Atomická operace je nedělitelná – dokončí se úplně nebo vůbec, bez mezilehlého stavu viditelného pro ostatní vlákna.std::atomicposkytuje atomický přístup k hodnotě typuTbez vyžadování mutexu.
Zvažte jednoduchý++counterna běžnémintsdíleném mezi vlákny. To jsou vlastně tři kroky: načíst, zvýšit, uložit. Mezi těmito kroky se může prokládat další vlákno, což způsobí datový závod (nedefinované chování). Sstd::atomicje přírůstek jedinou atomickou operací – žádné prokládání, žádné UB.
Ale atomy jdou hlouběji než jen „nedělitelné operace“. Řídí také řazení paměti — pravidla, kterými se řídí zápisy jedním vláknem, se stanou viditelnými pro ostatní vlákna. To je místo, kde atomy získávají sílu i jemnost.
std::atomické základy
Nejběžnější atomické operace jsouload(),store(),exchange()afetch_add()/fetch_sub(). Můžete také použít operátory++,--,+=atd.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter{0}; // atomic integer, initialized to 0
void increment(int n) {
for (int i = 0; i < n; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
// or simply: ++counter; (uses seq_cst by default)
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment, 100'000);
}
for (auto& t : threads) t.join();
std::cout << "Counter: " << counter.load() << "\n";
// Always prints 1000000 — no data race, no mutex needed
return 0;
}Vysvětlení řazení paměti
Moderní procesory a kompilátory přenastavují paměťové operace pro výkon. Na x86 je to relativně mírné, ale na ARM/RISC-V je to agresivní. Uspořádání paměti sděluje kompilátoru a CPU, které změny uspořádání jsou povoleny.
memory_order_seq_cst (výchozí) — Nejsilnější řazení. Všechna vlákna vidí všechny operaceseq_cstve stejném celkovém pořadí. Jednoduché a bezpečné, ale může omezit optimalizaci.
memory_order_acquire — Používá se u zatížení. Žádné čtení/zápis paměti v aktuálním vláknu nelze změnit před tímto načtením. Přemýšlejte o tom jako: "vše, co jsem si přečetl po tomto bodě, vidí alespoň to, co napsalo uvolňující vlákno."
memory_order_release — Používá se v obchodech. Žádné čtení/zápis paměti v aktuálním vláknu nelze změnit po tomto úložišti. Přemýšlejte o tom jako: "vše, co jsem napsal před tímto bodem, je viditelné pro přijímající vlákno."
memory_order_acq_rel — Kombinuje získání a uvolnění. Používá se při operacích čtení-upravování-zápis, jako jecompare_exchange.
memory_order_relaxed — Žádné záruky objednání nad rámec atomicity. Operace je atomická, ale lze ji libovolně měnit. Používejte pouze pro nezávislá počítadla nebo statistiky, kde na pořadí nezáleží.
Získání a uvolnění v praxi
Vzor získání-uvolnění je tahounem programování bez zámku. Úložiště vydání v jednom vlákně se synchronizuje s načtením v jiném vlákně, čímž se vytvoří vztah „předtím“.
#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> data_ready{false};
int payload = 0; // non-atomic — protected by acquire-release
void producer() {
payload = 42; // write payload BEFORE the release store
data_ready.store(true, std::memory_order_release);
// release: all writes before this store are visible
// to a thread that does an acquire load of data_ready
}
void consumer() {
while (!data_ready.load(std::memory_order_acquire)) {
// spin — acquire: sees all writes before the release store
}
// guaranteed to see payload == 42
assert(payload == 42); // never fires
std::cout << "Payload: " << payload << "\n";
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}Vzor porovnat a vyměnit (CAS).
Compare-and-swap (compare_exchange_weak/compare_exchange_strong) je základním stavebním kamenem bezzámkových datových struktur. Atomicky kontroluje, zda se hodnota rovná očekávané hodnotě, a pokud ano, nahradí ji požadovanou hodnotou.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> max_value{0};
void update_max(int new_val) {
int current = max_value.load(std::memory_order_relaxed);
// CAS loop: keep trying until we succeed or new_val is not bigger
while (new_val > current) {
if (max_value.compare_exchange_weak(
current, // expected (updated on failure)
new_val, // desired
std::memory_order_relaxed)) {
break; // success
}
// On failure, 'current' is updated with the actual value
// Loop re-checks if new_val > current (maybe another thread won)
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 100; ++i) {
threads.emplace_back(update_max, i);
}
for (auto& t : threads) t.join();
std::cout << "Max: " << max_value.load() << "\n"; // always 99
return 0;
}Spinlock s std::atomic_flag
std::atomic_flagje nejprimitivnější atomový typ – zaručeně bez uzamčení na každé platformě. Podporuje pouzetest_and_set()aclear(), takže je ideální pro jednoduchý spinlock.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
class SpinLock {
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {
// spin — consider adding a pause/yield hint:
// __builtin_ia32_pause() or std::this_thread::yield()
}
}
void unlock() {
flag_.clear(std::memory_order_release);
}
};
SpinLock spin;
int shared_data = 0;
void work(int n) {
for (int i = 0; i < n; ++i) {
spin.lock();
++shared_data; // protected by spinlock
spin.unlock();
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(work, 100'000);
}
for (auto& t : threads) t.join();
std::cout << "shared_data: " << shared_data << "\n";
// prints 400000
return 0;
}memory_order_relaxedposkytuje žádné záruky objednávek kromě atomicity samotné. Jeho použití k ochraně sdílených neatomových dat (jako je vlajka střežící užitečné zatížení) je chyba:
// BUG: relaxed store/load does not synchronize payload
payload = 42;
ready.store(true, memory_order_relaxed); // might be reordered BEFORE payload=42!
// Consumer:
while (!ready.load(memory_order_relaxed)) {}
assert(payload == 42); // MAY FAIL on ARM/POWERUse pořadí_paměti_relaxed only when the atomic variable stands alone and doesn't need to synchronize access to other data. For publish-consume patterns, use release/acquire.
Začněte smemory_order_seq_cst(výchozí) a uvolněte se ke slabším objednávkám, až když to profilování ukáže jako nezbytné. Nesprávné uspořádání paměti vytváří chyby, které je téměř nemožné reprodukovat na x86, ale projevují se na ARM. Preferujte mutexy pro komplexní sdílený stav — atomics jsou pro jednoduché sdílené proměnné a vytváření primitiv bez zámku. Vždy ověřte předpoklady bez zámku pomocístd::atomic.
std::atomicposkytuje nedělitelné operace bez mutexů — bezpečné pro souběžné čtení a zápis- Řazení paměti řídí, jak se zápisy v jednom vlákně stanou viditelnými pro ostatní vlákna – nejde ne jen o atomicitu
- Acquire-release je klíčový vzor: release-store publikuje data, purchase-load je spotřebovává
compare_exchange_weak/strong(CAS) je stavební blok pro algoritmy bez zámkustd::atomic_flagje jediný typ zaručeně bez uzamčení na všech platformách- Ve výchozím nastavení je
seq_csta oslabuje se pouze tehdy, když to vyžaduje profilování – jemné chyby se extrémně obtížně ladí
Kvíz — Otestujte své znalosti
(25 XP)1. Co zaručuje`memory_order_acquire`při operaci zatížení?
2. Proč se`compare_exchange_weak`obvykle používá ve smyčce?
3. Kdy je bezpečné používat`memory_order_relaxed`?