Demystifying Memory Ordering in C++
If you’ve ever wondered why your lock-free queue is slower than a mutex, you will have a better view on things by the end of this article.

Your code doesn’t necessarily get executed in the order you wrote it.
Demystifying Memory Ordering in C++: Inaugural
If you’ve ever wondered why your lock-free queue is slower than a mutex, you will have a better view on things by the end of this article.
As a System Programming Language, C++ is all about being as “close to the machine” as possible, so close that there would be no need for lower level languages specially when it comes to Memory Manipulation and in a way that is C++’s super power!
Memory Model Basics
Starting with the basic definitions we have two main aspect categories both of which are interrelated:
- structural aspects [related to how things are laid out in memory locations].
- concurrency aspects [related to multiple threading].
If multiple threads try to access separate memory locations there is really no problem here. Big problems arise when threads access the same memory location without synchronization, and at least one access is a write, that causes what is commonly known as a data race which puts you in one of the nastiest corners of C++ Undefined Behaviors.
Every C++ developer knows std::atomic is a go-to fix for these issues. But most treat Memory Ordering like black magic, defaulting to std::memory_order_seq_cst and hoping for the best. This approach works, but it may be silently killing your performance.
There’s a reason lock-free legends use acquire, release, and relaxed and it’s not just to show off. In this article we will deep dive, breaking down the six memory orderings and exposing the three patterns that cover 90% of real-world concurrency to show you exactly when you can drop the safety rails without falling off.
C++ Memory Ordering: What acquire and release Actually Mean
Here’s a confession: most C++ developers (myself included, not long ago) slap std::memory_order_seq_cst on every other atomic operation and hope for the best. It works. It’s safe. But it’s also leaving performance on the table and more importantly, it means you don’t actually understand what’s happening under the hood.
This article will change that. By the end, you’ll know exactly when to use each memory order, why acquire and release form the most important pattern, and how to reason about concurrent code without guesswork.
Prerequisites: Basic familiarity with std::atomic and std::thread.
Why Memory Ordering Exists!
Your C++ code doesn’t run the way you wrote it!
Both the compiler and the CPU reorder operations to enhance performance. This is fine in single-threaded code the “as-if” rule guarantees observable behavior stays the same. But in multithreaded code? Reordering creates chaos.
// Thread 1
data = 42;
ready = true;
// Thread 2
if (ready)
std::cout << data; // Could print 0!
Without memory ordering, Thread 2 might see ready == true before it sees data == 42. The CPU or compiler could have reordered the stores in Thread 1, or the loads in Thread 2.
The C++ Memory Orders From Weakest to Strongest
C++ gives you six memory orders. Here they are, ranked by strength:
Order Strength Use Case
relaxed Weakest Counters, statistics
consume Weak (Avoid poorly supported)
acquire Medium Reading shared state
release Medium Publishing shared state
acq_rel Strong Read-modify-write operations
seq_cst Strongest Default, full ordering
memory_order_relaxed
No synchronization. No ordering guarantees. Only atomicity is guaranteed.
std::atomic<int> counter{0};
void increment() {
// Safe: atomic increment, but no ordering with other operations
counter.fetch_add(1, std::memory_order_relaxed);
}
int get_count() {
return counter.load(std::memory_order_relaxed);
}
When to use: Statistics, counters, progress indicators anything where you don’t need to synchronize other data with this value.
memory_order_consume
Skip this one. It’s designed for dependency chains but no major compiler implements it correctly. They all promote it to acquire. The C++ committee is reworking it.
memory_order_acquire and memory_order_release
This is the most important pair. Master this and you’ve mastered 90% of practical memory ordering.
Release: “I’m done writing. Publish everything before this point.” Acquire: “I’m about to read. Make sure I see everything published.”
std::atomic<bool> ready{false};
int data = 0; // non-atomic!
// Thread 1 (Producer)
void producer() {
data = 42; // A
ready.store(true, std::memory_order_release); // B: release barrier
}
// Thread 2 (Consumer)
void consumer() {
while (!ready.load(std::memory_order_acquire)) {} // C: acquire barrier
assert(data == 42); // D: guaranteed to see 42!
}
What happens:
- The
releaseat B ensures A happens-before B - The
acquireat C synchronizes with thereleaseat B - Therefore, A happens-before D
data == 42is guaranteed
Visual:
Thread 1: Thread 2:
data = 42
│ │
│
ready = true ───┼──► ready == true
(release) │ (acquire)
│ │
└──────────
data == 42
memory_order_acq_rel
Combines acquire and release. Use for read-modify-write operations that both consume and publish.
std::atomic<int> ref_count{1};
void release_reference() {
// Acquire: see all writes before we potentially delete
// Release: our "I'm done" is visible to others
if (ref_count.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete resource;
}
}
memory_order_seq_cst
Sequential consistency. The default. All threads see all seq_cstoperations in the same global order.
std::atomic<bool> x{false};
std::atomic<bool> y{false};
std::atomic<int> z{0};
void write_x() { x.store(true, std::memory_order_seq_cst); }
void write_y() { y.store(true, std::memory_order_seq_cst); }
void read_x_then_y() {
while (!x.load(std::memory_order_seq_cst)) {}
if (y.load(std::memory_order_seq_cst)) ++z;
}
void read_y_then_x() {
while (!y.load(std::memory_order_seq_cst)) {}
if (x.load(std::memory_order_seq_cst)) ++z;
}
// After all threads complete: z > 0 is GUARANTEED
// At least one reader sees both flags as true
Cost: Global synchronization is expensive. On x86 it’s not terrible, but on ARM/PowerPC it can be significant.
What’s Next
You now understand the six memory orderings and the guarantees each provides from relaxed(just atomicity) to seq_cst(global ordering). You know why your code doesn’t run the way you wrote it, and you’ve seen the release/acquirehandshake that forms the backbone of most synchronization.
But knowing the tools isn’t the same as knowing when to use them.
In the upcoming article, we’ll move from theory to practice. I’ll show you the three patterns that cover 90% of real-world concurrency: producer-consumer, reference counting, and spinlocks. We’ll build a lock-free queue, benchmark the performance difference between orderings, and give you a decision flowchart you can actually use.
Because understanding memory ordering isn’t about memorizing rules it’s about recognizing patterns.
Reference: This article draws from Chapter 5 of “C++ Concurrency in Action” by Anthony Williams (2nd Edition), specifically sections 5.2 and 5.3. If you want the full deep-dive:
- Chapter 5 (entire): The definitive reference on the C++ memory model
- Section 5.2: Atomic operations and types
- Section 5.3: Synchronization and ordering constraints
메타데이터
- post_id
- 4cb2b6aaeae8
- slug
- demystifying-memory-ordering-in-c-4cb2b6aaeae8
- url
- https://medium.com/womenintechnology/demystifying-memory-ordering-in-c-4cb2b6aaeae8
- canonical_url
- https://medium.com/womenintechnology/demystifying-memory-ordering-in-c-4cb2b6aaeae8
- author_url
- https://medium.com/@bits_and_giggles
- status
- ok
- fetched_at
- 2026-07-09 16:18:44