C/C++ Arena

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.

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

Practice it