Sitemap
W

W

I'm a software engineer with a passion for technology, science, and lifelong learning.

C++ 1

3 min readOct 22, 2026

--

For std::atomic<bool> flag{false}; used to publish data between threads, which is the minimal correct ordering pair for the classic publish/subscribe pattern?
Answer: release store / acquire load

This question is testing one of the central ideas in the C++ memory model: how one thread safely publishes ordinary, non-atomic data to another thread using an atomic flag.

Consider the classic pattern:

#include <atomic>
int data;
std::atomic<bool> flag{false};
// Thread A — publisher
data = 42;
flag.store(true, std::memory_order_release);
// Thread B — subscriber
if (flag.load(std::memory_order_acquire)) {
std::cout << data; // guaranteed to see 42
}

The minimal correct pair is:

publisher: store(..., std::memory_order_release)
subscriber: load(..., std::memory_order_acquire)

1. What problem are we solving?

flag itself is atomic, so there is no data race on flag.

But data is not atomic:

int data;

Thread A writes:

data = 42;

while Thread B later reads:

std::cout << data;

We need to establish an ordering relationship saying:

Thread A                         Thread B
data = 42
│
▼
flag.store(true) ─────────► flag.load() == true
│
▼
read data

Conceptually, we want:

write data
↓
release store
↓
──────── synchronization ────────
↓
acquire load
↓
read data

2. What does release do?

data = 42;
flag.store(true, std::memory_order_release);

The release operation says, roughly:

Everything this thread did before this release must be published before another thread successfully acquires this value.

So the compiler/CPU cannot make the publication behave as though the flag became visible before the preceding data writes were properly published.

Get W’s stories in your inbox

Join Medium for free to get updates from this writer.

Think of release as:

prepare everything
↓
↓
PUBLISH

3. What does acquire do?

On the consumer:

if (flag.load(std::memory_order_acquire)) {
std::cout << data;
}

Acquire says, roughly:

Once I observe the value published by a release operation, subsequent operations must observe the effects that happened before that release.

Think of it as:

observe PUBLISHED
↓
↓
use published data

4. The formal C++ relationship

Suppose:

// Thread A
data = 42; // A
flag.store(true, std::memory_order_release); // B

// Thread B
bool x = flag.load(std::memory_order_acquire); // C
if (x)
std::cout << data; // D

If C reads the true written by B, then:

A  sequenced-before  B
B synchronizes-with C
C sequenced-before D

Therefore:

A happens-before D

or visually:

data = 42
│
│ sequenced-before
▼
release store(true)
│
│ synchronizes-with
▼
acquire load() == true
│
│ sequenced-before
▼
read data

That happens-before relationship is the key. It makes Thread B’s read of data properly synchronized with Thread A's write.

5. Why isn’t relaxed enough?

Suppose we used:

// Thread A
data = 42;
flag.store(true, std::memory_order_relaxed);

// Thread B
if (flag.load(std::memory_order_relaxed))
std::cout << data;

Atomicity of flag is still guaranteed. Thread B can safely read flag.

But seeing:

flag == true

does not establish a happens-before relationship for data.

So relaxed atomics solve:

atomic access to flag

but not:

publication of data

That’s a very important distinction.

6. Why is this called the “minimal” correct ordering?

You could use:

flag.store(true, std::memory_order_seq_cst);
flag.load(std::memory_order_seq_cst);

and it would work.

seq_cst is stronger, though. For this simple one-way publication, you don't need a global sequentially consistent ordering. You only need:

Publisher                    Subscriber
ordinary writes
│
▼
release ───────────────► acquire
│
▼
ordinary reads

Therefore:

release / acquire

is the minimal standard memory-ordering pair that directly expresses the required synchronization.

One subtle point: the acquire load only gets this guarantee if it actually observes the release-published value (or an applicable release sequence). Simply performing an acquire load isn’t enough by itself.

A useful mental model

When you encounter this pattern, think:

PUBLISHER                         SUBSCRIBER

construct/change data
│
▼
RELEASE store
│
│
└──────────────────────┐
│ synchronization
▼
ACQUIRE load
│
▼
consume data

So a good rule of thumb is:

Release publishes what came before; acquire makes the published state visible to what comes after.

This same release/acquire concept is foundational for understanding mutexes, spinlocks, lock-free queues, reference counting, and many other concurrent C++ algorithms.

--

--

W
W

Written by W

I'm a software engineer with a passion for technology, science, and lifelong learning.