C++ Systems Engineering / Under the Hood

C++ Atomics and Memory Ordering: Publish Data, Not Just a Flag

Understand atomicity, release/acquire publication, relaxed counters, and object lifetime. Use a complete one-shot example without mistaking atomics for a general lock-free design.

Palash Vij / Published / 3 min read
Source-backed explainer C++ Atomics Memory ordering Concurrency

Replacing a shared Boolean with std::atomic<bool> removes a race on that Boolean. It does not automatically make every nearby object safe to access from another thread.

Atomicity and ordering solve related but different problems. Atomic operations prevent conflicting accesses to the atomic object from tearing or forming a data race. Ordering can establish how other operations become visible across threads under the C++ memory model.

Publish a payload once

In this one-shot example, one producer writes an ordinary integer, then publishes readiness with a release store. The consumer waits until an acquire load observes that store before reading the payload.

#include <atomic>
#include <cassert>
#include <thread>

int main() {
  int payload = 0;
  std::atomic<bool> ready{false};

  std::thread producer([&] {
    payload = 42;
    ready.store(true, std::memory_order_release);
  });

  std::thread consumer([&] {
    while (!ready.load(std::memory_order_acquire)) {
      std::this_thread::yield();
    }
    assert(payload == 42);
  });

  producer.join();
  consumer.join();
}

Save as publication.cpp and compile with a thread-capable toolchain:

g++ -std=c++11 -O2 -Wall -Wextra -pthread publication.cpp -o publication-demo
./publication-demo

The example should exit without output. It is a functional demonstration, not a throughput test. yield is not a robust waiting policy for a long-running production service; condition variables or suitable atomic-wait facilities can avoid spinning.

Follow the happens-before argument

The payload write is sequenced before the release store. An acquire load that observes that publication synchronizes with it. The payload read after the successful acquire is therefore ordered after the producer’s write.

That argument depends on the actual publication event. An acquire operation is not a universal cache refresh, and a release operation is not permission to access arbitrary shared objects without checking their ownership and lifetime.

Changing both operations to relaxed would keep the flag atomic but remove the publication ordering needed for the ordinary payload access. A test may appear to work on a particular machine and still fail to establish a valid C++ program.

Relaxed counters have a narrower purpose

A standalone telemetry counter that does not publish other state may only need atomic increments and loads with relaxed ordering. Even then, reading several independent counters does not produce a coherent multi-field snapshot.

Use release/acquire when the protocol requires a publication relationship, and stronger ordering when the algorithm needs it. Sequential consistency is easier to reason about for many uses, but it does not fix a broken lifetime protocol or turn several operations into a transaction.

Do not choose weaker ordering solely because its name sounds faster. Measure after writing a correctness argument.

Reuse changes the problem

This example is safe because publication happens once and the payload is not rewritten while the consumer reads it. Resetting the flag and repeatedly modifying the payload introduces a new protocol with possible overlap.

Likewise, publishing a pointer does not keep the pointed-to object alive. Lock-free structures need reclamation rules so a reader cannot dereference an object another thread freed or reused. ABA, reference lifetimes, and progress guarantees do not disappear because the pointer itself is atomic.

An atomic type is also not guaranteed to be lock-free on every platform. Check the implementation’s properties when that guarantee matters.

Test and measure the right things

Use concurrency stress tests and a race detector where supported, and test on the architectures you intend to support. These tools help find bugs but cannot replace the memory-model argument.

For performance, record compiler/version, optimization flags, hardware, thread placement, operation count, contention, and run distributions. Shared cache-line traffic or false sharing may dominate even when every operation is logically independent.

If the requirement is simply a bounded producer/consumer handoff, a mutex and condition-variable queue may be clearer. A small amount of synchronization that is demonstrably correct is preferable to a lock-free claim without a reclamation or shutdown story.

Further reading

Primary references behind this explanation. Consult documentation for the version and environment you actually run.

  1. C++ working draft: multithreaded executions and data races
  2. C++ working draft: atomic ordering
  3. cppreference: memory_order
Follow the thread

Related reading

Search the notebook

Search articles, projects, and experiments. Everything stays on your device.

palash / notebook

A small doorway into the site. No shell, no remote execution.

Welcome. Type help to find your way around.

lab →experience →contact →Ctrl / Cmd + K