Concurrency Atomic and volatile in C11 memory model
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
When developers compare atomic and volatile, the most important fact is simple: volatile is not a thread-synchronization primitive. In modern C and C++ memory models, atomics are for inter-thread coordination, while volatile is mainly for special memory locations such as hardware registers or memory-mapped I/O.
What Atomics Actually Guarantee
Atomic variables provide two things that ordinary shared variables do not:
- operations that are indivisible with respect to other threads
- memory-ordering rules that let threads synchronize correctly
In C++11, the API is std::atomic.
This is safe because the increment is atomic. Two threads can update the same variable without creating a data race.
What volatile Does Not Guarantee
volatile tells the compiler that reads and writes should not be optimized away or silently cached in a register across accesses. It does not make compound operations atomic and does not create the happens-before relationships needed for thread synchronization.
This code is still broken in a multithreaded program. ++counter is a read-modify-write sequence, and volatile does not make that sequence safe.
Memory Ordering Matters
Atomics are more than "thread-safe variables." They also let you choose memory ordering semantics.
- '
memory_order_relaxedfor atomicity without ordering guarantees' - '
memory_order_releaseandmemory_order_acquirefor handoff patterns' - '
memory_order_seq_cstfor the strongest and simplest global ordering model'
A simple flag handoff looks like this:
The release-store and acquire-load create the synchronization that makes data = 42 visible correctly.
When volatile Is Appropriate
volatile still has legitimate uses, just not ordinary thread communication. Typical cases include:
- memory-mapped device registers
- signal-handler-visible state in narrow scenarios
- hardware interaction where every load and store must really happen
Those are unusual cases compared with everyday application concurrency. If the goal is "make two threads see the same updates safely," the answer is almost never volatile.
The Most Common Misunderstanding
Many developers learn that volatile prevents optimizations and then assume that this also solves visibility and ordering between threads. It does not. A program can use volatile everywhere and still have undefined behavior from data races.
That is why the modern rule is so blunt: if shared state is accessed concurrently without stronger external synchronization, use atomics or locks, not volatile.
Locks Are Still Part of the Story
Atomics are not a replacement for every lock. They are ideal for counters, flags, and carefully designed lock-free structures, but once shared state spans several related fields, a mutex is often simpler and safer. The real lesson is not "use atomics everywhere." The real lesson is "use atomics for atomic state, and use locks when consistency across multiple values matters."
Common Pitfalls
- Using
volatileas though it made shared variables safe between threads. - Thinking atomicity alone is enough and ignoring memory ordering requirements.
- Using non-atomic side data next to atomic flags without proper acquire-release synchronization.
- Reaching for
memory_order_relaxedeverywhere before understanding the visibility implications. - Assuming hardware-register use cases for
volatileapply to ordinary application concurrency.
Summary
- Atomics are for safe concurrent access and memory ordering.
- '
volatileis mainly for special memory access semantics, not thread synchronization.' - A
volatileincrement is still a race. - Acquire-release and sequentially consistent orderings exist for a reason and should be chosen deliberately.
- For thread communication, use atomics or locks, not
volatile.

