C++ memory ordering: relaxed, acquire and release
What std::memory_order means, why compilers and processors reorder memory accesses, and how release and acquire publish data safely between threads.
Compilers and processors reorder memory accesses to go faster. Inside one thread you can never tell, but another thread can see your writes in a different order than you made them. Every atomic operation takes a memory order that says which reorderings are forbidden around it.
memory_order_seq_cst(the default): all threads agree on one order of every such operation. The easiest to reason about, and the right choice unless profiling says otherwise.memory_order_releaseon a store: nothing written before it in this thread can move after it.memory_order_acquireon a load: nothing read or written after it can move before it.- When an acquire load reads the value a release store wrote, everything the writing thread did before the store is visible to the reading thread after the load. That's how one thread publishes ordinary data to another.
memory_order_relaxed: the operation is still indivisible, but it orders nothing else. Fine for a statistics counter read afterjoin(), wrong for a "the data is ready" flag.
Code with orderings that are too weak often works on x86, which reorders very little, and then fails on ARM phones and servers. Test on ARM and run ThreadSanitizer (-fsanitize=thread), which understands orderings. The browser compiler on this site has no threads, so this example was compiled with g++ -std=c++20 -pthread and run on Linux.
Example
#include <atomic>
#include <iostream>
#include <numeric>
#include <thread>
#include <vector>
std::vector<int> data; // ordinary data, published by `ready`
std::atomic<bool> ready{false};
std::atomic<int> hits{0}; // a plain counter: relaxed is enough
int main() {
std::thread producer([] {
for (int i = 1; i <= 100; i++) data.push_back(i);
ready.store(true, std::memory_order_release);
});
std::vector<std::thread> readers;
for (int t = 0; t < 4; t++)
readers.emplace_back([] {
while (!ready.load(std::memory_order_acquire)) {
}
for (int i = 0; i < 1000; i++) hits.fetch_add(1, std::memory_order_relaxed);
if (std::accumulate(data.begin(), data.end(), 0) != 5050) std::cout << "torn read!\n";
});
producer.join();
for (auto& r : readers) r.join();
std::cout << "sum " << std::accumulate(data.begin(), data.end(), 0) << ", hits " << hits.load() << "\n";
return 0;
}
Output (compiled with GCC and run on Linux: the in-browser compiler has no threads):
sum 5050, hits 4000