Operații atomice și ordonarea memoriei
Înțelegeți programarea fără blocare cu std::atomic. Comandări de memorie master (relaxat, achiziționare, eliberare, seq_cst), implementați un spinlock cu atomic_flag și învățați modelul de comparare și schimbare (CAS).
Ce sunt operațiunile atomice?
O operație atomică este indivizibilă - se finalizează în întregime sau deloc, fără nicio stare intermediară vizibilă pentru alte fire.std::atomicoferă acces atomic la o valoare de tipTfără a necesita un mutex.
Luați în considerare un simplu++counterpe unintobișnuit partajat între fire. Acesta este de fapt trei pași: încărcare, creștere, stocare. Un alt fir se poate intercala între acești pași, provocând o cursă de date (comportament nedefinit). Custd::atomic, incrementul este o singură operație atomică - fără intercalare, fără UB.
Dar atomica merge mai adânc decât doar „operațiuni indivizibile”. De asemenea, controlează ordonarea memoriei — regulile care guvernează atunci când scrierile de către un fir devin vizibile pentru alte fire. Aici atomii devin atât puternici, cât și subtil.
std::atomic Basics
Cele mai comune operațiuni atomice suntload(),store(),exchange()șifetch_add()/fetch_sub(). De asemenea, puteți utiliza operatorii++,--,+=etc.
#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;
}Ordinea memoriei explicată
CPU-urile și compilatoarele moderne reordonează operațiunile de memorie pentru performanță. Pe x86 acest lucru este relativ ușor, dar pe ARM/RISC-V este agresiv. Ordinea memoriei indică compilatorului și procesorului care reordonări sunt permise.
memory_order_seq_cst (implicit) — Cea mai puternică comandă. Toate firele văd toate operațiunileseq_cstîn aceeași ordine totală. Simplu și sigur, dar poate limita optimizarea.
memory_order_acquire — Folosit pe încărcări. Nicio citire/scriere în memorie în firul curent nu poate fi reordonată înainte de această încărcare. Gândiți-vă la asta ca: „tot ce am citit după acest punct vede cel puțin ceea ce a scris firul de lansare”.
memory_order_release — Folosit în magazine. Nicio citire/scriere de memorie în firul curent nu poate fi reordonată după acest magazin. Gândiți-vă la asta ca: „tot ce am scris înainte de acest punct este vizibil pentru firul de achiziție”.
memory_order_acq_rel — Combinele achiziționează și lansează. Folosit la operațiuni de citire-modificare-scriere precumcompare_exchange.
memory_order_relaxed — Nu există garanții de comandă dincolo de atomicitate. Operația este atomică, dar poate fi reordonată liber. Folosiți numai pentru contoare sau statistici independente, unde comanda nu contează.
Acquire-Release în practică
Modelul de achiziție-eliberare este calul de lucru al programării fără blocare. Un magazin de lansare într-un fir se sincronizează cu o încărcare de achiziție dintr-un alt fir, stabilind o relație de întâmplare înainte.
#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;
}Model de comparare și schimbare (CAS).
Compararea și schimbarea (compare_exchange_weak/compare_exchange_strong) este elementul fundamental al structurilor de date fără blocare. Verifică atomic dacă valoarea este egală cu o valoare așteptată și, dacă da, o înlocuiește cu o valoare dorită.
#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 cu std::atomic_flag
std::atomic_flageste cel mai primitiv tip atomic - garantat fără blocare pe fiecare platformă. Acceptă doartest_and_set()șiclear(), făcându-l perfect pentru un simplu 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_relaxedoferă nicio garanție de comandă dincolo de atomicitatea în sine. Folosirea acestuia pentru a proteja datele non-atomice partajate (cum ar fi un steag care protejează o sarcină utilă) este o eroare:
// 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 memory_order_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.
Începeți cumemory_order_seq_cst(prestabilit) și relaxați-vă doar la comenzi mai slabe atunci când profilarea dovedește că este necesar. Comandările incorecte ale memoriei produc erori care sunt aproape imposibil de reprodus pe x86, dar se manifestă pe ARM. Preferați mutexurile pentru starea partajată complexă - atomii sunt pentru variabile simple partajate și pentru a construi primitive fără blocare. Verificați întotdeauna ipotezele fără blocare custd::atomic.
std::atomicoferă operații indivizibile fără mutexuri - sigur pentru citiri și scrieri simultane- Ordinea memoriei controlează modul în care scrierile dintr-un fir devin vizibile pentru alte fire - nu este vorba doar despre atomicitate
- Acquire-release este modelul cheie: release-store publică date, achiziționare-încărcare le consumă
compare_exchange_weak/strong(CAS) este blocul de bază pentru algoritmi fără blocarestd::atomic_flageste singurul tip garantat fără blocare pe toate platformele- Implicit la
seq_cstși slăbiți numai atunci când profilarea o cere - erorile subtile sunt extrem de greu de depanat
Test — Verifică-ți cunoștințele
(25 XP)1. Ce garantează`memory_order_acquire`la o operație de încărcare?
2. De ce este utilizat de obicei`compare_exchange_weak`într-o buclă?
3. Când este`memory_order_relaxed`sigur de utilizat?