C/C++ Arena

Step 4 of 6

Atomics

For a single counter or flag, a mutex is heavy machinery. std::atomic<T> (from <atomic>) makes individual operations on one variable indivisible: no other thread can ever see them half done, and they don't form data races. For int and friends, the processor does this with special instructions, no lock needed.

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

int main() {
    std::atomic<long long> counter{0};
    auto work = [&] {
        for (int i = 0; i < 1000000; i++) counter++;   // one indivisible read-add-write
    };
    std::thread a(work), b(work);
    a.join();
    b.join();
    std::cout << counter.load() << "\n";
}
2000000

Atomics work in the browser too (there's just one thread to use them), so the rest of this step runs here.

The operations

#include <atomic>
#include <iostream>

int main() {
    std::atomic<int> x{10};

    x.store(20);                     // write
    int now = x.load();              // read
    int before = x.fetch_add(5);     // add 5, return the value from BEFORE
    int old = x.exchange(100);       // set to 100, return the old value

    int expected = 100;
    bool swapped = x.compare_exchange_strong(expected, 7);   // 100 -> 7
    int expected2 = 100;
    bool swapped2 = x.compare_exchange_strong(expected2, 8); // x is 7 now: fails

    std::cout << now << " " << before << " " << old << "\n";
    std::cout << swapped << " " << swapped2 << " " << expected2 << " " << x.load() << "\n";
}
20 20 25
1 0 7 7

Check-then-act needs CAS

Each operation is atomic, but two operations in a row are not. Keeping a shared maximum this way is a race:

if (value > best.load()) best.store(value);   // another thread can store in between

Between the load and the store, another thread can store a bigger value, which this store then wipes out. The fix is a CAS loop: read the current value, and only replace it if nothing changed in the meantime; if something did, expected now holds the new value, so check again.

int cur = best.load();
while (value > cur && !best.compare_exchange_weak(cur, value)) {
    // cur was refreshed with the latest value; the loop re-checks it
}

compare_exchange_weak may occasionally fail even when the values match (it's cheaper on some processors), which is fine inside a loop. Use _strong for a single attempt.

Atomics suit single variables: counters, flags, the index of the next job. As soon as several variables must change together (a balance and a transaction list), use a mutex.

Your turn: write take_ticket (returns the next ticket number and advances the counter, like a deli ticket machine), claim (returns true only for the first caller ever, using the flag), and update_max (raises best to value if value is bigger, with a CAS loop).

Previous: Mutexes and lock_guard Next: Deadlock and lock ordering