Přeskočit na obsah

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.

atomic_counter.cpp
#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“.

acquire_release.cpp
#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.

cas_max.cpp
#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.

spinlock.cpp
#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;
}
Úskalí

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/POWER

Use 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.

Osvědčený postup

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::is_lock_free().

Klíčové poznatky
  • 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ámku
  • std::atomic_flagje jediný typ zaručeně bez uzamčení na všech platformách
  • Ve výchozím nastavení jeseq_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`?