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
fetch_addreturns the old value. That makes it perfect for handing out unique numbers: two threads calling it at the same moment always get different results.exchangesets a new value and returns the old one in one step. If a thread swapstrueinto a flag and getsfalseback, it knows it was the first.compare_exchange_strong(expected, desired)(CAS, compare-and-swap) setsdesiredonly if the current value equalsexpected, and returns whether it did. When it fails, it copies the current value intoexpected, which is whyexpected2became 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