Concurrency
C++11
Atomic
Volatile
Memory Model

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.

cpp
1#include <atomic>
2#include <thread>
3#include <iostream>
4
5std::atomic<int> counter{0};
6
7void worker() {
8    for (int i = 0; i < 10000; ++i) {
9        counter.fetch_add(1, std::memory_order_relaxed);
10    }
11}
12
13int main() {
14    std::thread t1(worker);
15    std::thread t2(worker);
16    t1.join();
17    t2.join();
18    std::cout << counter.load() << "
19";
20}

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.

cpp
1volatile int counter = 0;
2
3void worker() {
4    for (int i = 0; i < 10000; ++i) {
5        ++counter;
6    }
7}

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_relaxed for atomicity without ordering guarantees'
  • 'memory_order_release and memory_order_acquire for handoff patterns'
  • 'memory_order_seq_cst for the strongest and simplest global ordering model'

A simple flag handoff looks like this:

cpp
1#include <atomic>
2#include <thread>
3#include <iostream>
4
5std::atomic<bool> ready{false};
6int data = 0;
7
8void producer() {
9    data = 42;
10    ready.store(true, std::memory_order_release);
11}
12
13void consumer() {
14    while (!ready.load(std::memory_order_acquire)) {
15    }
16    std::cout << data << "
17";
18}

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 volatile as 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_relaxed everywhere before understanding the visibility implications.
  • Assuming hardware-register use cases for volatile apply to ordinary application concurrency.

Summary

  • Atomics are for safe concurrent access and memory ordering.
  • 'volatile is mainly for special memory access semantics, not thread synchronization.'
  • A volatile increment 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.

Course illustration
Course illustration

All Rights Reserved.