cswork / libraryOpen practice workspace

Articles

C++ Atomic Interview Questions: Memory Ordering, Happens-Before, and Data Races

Work through C++ atomic interview questions on release-acquire publication, relaxed counters, happens-before, data races, and atomic versus lock-free.

C++ atomic interview questions ask you to prove which memory operations are ordered, which values can be observed, and whether any conflicting access remains unsynchronized. An atomic variable protects its own atomic operations. It does not automatically make the whole object or algorithm thread-safe.

The central tool is the happens-before relationship. If one thread publishes ordinary data and another reads it, you need a valid synchronization argument for that data as well as a safe object lifetime. “It works on my CPU” is not that argument.

This guide uses a one-time publication example and a relaxed statistics counter.

What is a data race in C++?

At a useful introductory level, a data race involves conflicting accesses from different threads, at least one non-atomic, without the required ordering relationship, subject to the language's detailed rules. A data race gives undefined behavior; it is not merely a chance of reading an old value.

Two threads incrementing an ordinary shared integer without synchronization are the classic example. Replacing the integer with an atomic can make those increments race-free for that variable, but associated ordinary fields may still be unsafe.

Distinguish a data race from a higher-level race condition. A program using only atomic operations can still implement the wrong protocol, lose a business update, or observe a combination of states the application did not intend. Race-free is an important property, not a complete specification of correctness.

In an interview, identify every shared location and every reader and writer. Then draw the order that protects each conflicting pair. If a field has no such argument, a comment saying “ready flag” does not repair it.

How does release-acquire publication work?

Consider one producer writing a payload once and one consumer reading it after publication:

#include <atomic>

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

void producer() {
    payload = 42;
    ready.store(true, std::memory_order_release);
}

int consumer() {
    while (!ready.load(std::memory_order_acquire)) {}
    return payload;
}

Under the stated one-time protocol, when the acquire load observes the relevant release store's value, the synchronization relationship orders the earlier payload write before the consumer's later read. The payload must remain alive and must not be concurrently modified afterward without additional synchronization.

The observation condition matters. Merely having one release operation and one acquire operation somewhere in the program does not create a useful edge between them. You must identify which atomic object connects the threads and what value or release-sequence relationship the read observes under the applicable standard rules.

Why does replacing both orders with relaxed break the argument?

Relaxed operations remain atomic for the atomic object, but they do not provide the same ordering of unrelated ordinary memory. If ready is only accessed with relaxed operations in the publication example, the flag alone does not establish the required happens-before edge for the ordinary payload.

The issue is not simply that the consumer “might see an old 0.” Without a valid synchronization relationship, the non-atomic payload accesses can form a data race under the program's concurrency, making the program invalid under the C++ memory model.

Do not defend the relaxed version by describing a particular cache-coherent CPU. The compiler and language rules matter too. Hardware behavior observed in a short test cannot establish that a program with undefined behavior is correct.

If the interviewer asks for a repair, choose a synchronization design you can prove. A mutex may be clearer when several fields and repeated updates are involved. Atomics are useful when their protocol remains manageable, not because a lock-free-looking snippet is automatically superior.

When is relaxed ordering useful?

An independent statistics counter can often use relaxed updates when it does not publish other data or decide whether unrelated ordinary memory may be accessed. The counter still requires a safe lifetime and a clear interpretation of its observed value.

std::atomic<unsigned long long> completed{0};

void record_completion() {
    completed.fetch_add(1, std::memory_order_relaxed);
}

If a monitor periodically reads this counter, ask whether an approximate observation is acceptable and what other state it uses. Reading “ten completed” does not automatically grant access to ten ordinary result objects unless another synchronization mechanism protects those results.

After worker threads have joined, the join relationship can provide ordering for work completed by those threads. Explain that separate synchronization if it is part of your proof. Do not attribute every guarantee in the program to the counter's memory order.

What does sequential consistency add?

Sequentially consistent atomic operations participate in a strong ordering model defined by the standard, including a single total order for the relevant sequentially consistent operations consistent with specified constraints. This often makes simple examples easier to reason about than weaker orders.

It does not turn an arbitrary collection of operations into one transaction. Two separately atomic loads can still observe values from different moments in an application protocol. Nor does it make unsynchronized ordinary data safe when the required relationship is missing.

A sensible interview approach is to start with a correct design using clear synchronization, then justify any weaker order with a specific proof and measured need. “Relaxed is faster” is not enough to choose it, and “seq_cst fixes everything” is not a proof either.

When asked about performance, state that costs depend on architecture, compiler, operation, and contention. Do not promise a universal ranking from the names alone.

Why is atomic increment different from load plus store?

Consider this pattern on an atomic integer:

auto value = counter.load();
counter.store(value + 1);

Each operation is atomic, but the pair is not an indivisible increment. Two threads can load the same old value and both store the same incremented value, losing one logical update. Use an atomic read-modify-write such as fetch_add when that is the intended operation.

This is a useful demonstration of a race condition without a data race on the atomic counter itself. The program satisfies the atomic access rules but fails the counting contract.

For more complex state changes, ask whether a compare-exchange loop, a mutex, or a redesigned ownership model is clearer. Several related fields cannot be treated as a single atomic state merely because each field is independently atomic.

How does compare_exchange work?

Compare-exchange conditionally updates an atomic value when it matches an expected value. On failure, the expected argument is updated with the observed value, which is important when writing a retry loop. The weak form may fail spuriously and is commonly used in loops.

int old = maximum.load(std::memory_order_relaxed);
while (old < candidate &&
       !maximum.compare_exchange_weak(
           old, candidate,
           std::memory_order_relaxed,
           std::memory_order_relaxed)) {
}

For this isolated numeric maximum, the loop retries using the refreshed old value. The relaxed choice assumes the maximum is an independent statistic, not a publication mechanism for other data. Change the application meaning and you may need a different synchronization design.

Success and failure memory orders have constraints in the API. A failed compare-exchange is a load rather than a successful read-modify-write, so release semantics do not belong on the failure path. Check the project's C++ standard and library documentation rather than copying arbitrary order pairs.

What is the ABA problem?

Suppose a thread observes pointer value A, pauses, and later sees A again. The underlying state may have changed from A to B and back to A in the meantime. A comparison of the pointer value alone does not reveal that history.

This matters in algorithms that interpret “still A” as proof that the object or structure is unchanged. Memory reuse can make the situation more dangerous: the same address may now refer to a different object lifetime.

Possible solutions depend on the algorithm and include versioned state, appropriate memory reclamation, or a simpler lock-based design. Adding a counter is not automatically sufficient if it wraps or if object reclamation remains unsafe. Explain the full invariant you need to preserve.

In an interview, draw the interleaving and identify the invalid assumption. The term ABA is less important than showing why equality of one value fails to prove continuity of the underlying state.

Is an atomic type always lock-free?

No. Atomic correctness and lock-free implementation are different properties. Support depends on type, size, alignment, implementation, and platform. Use the provided lock-free queries and documented guarantees when that property matters.

Also distinguish lock-free from wait-free at the algorithm level. A loop can guarantee system-wide progress without guaranteeing that each individual thread finishes within a bounded number of steps. A thread repeatedly losing a compare-exchange race may take many retries.

A design using a lock-free atomic primitive is not automatically a lock-free complete operation if it allocates memory, calls blocking code, or relies on other locks. Define the boundary of the progress claim before making it.

Why does volatile not replace atomics?

volatile has specific language and implementation uses, but it is not a general inter-thread synchronization mechanism in C++. Declaring a shared flag volatile does not establish the release-acquire relationship needed to publish ordinary data.

The same warning applies to adding arbitrary sleeps. Delaying a thread can make a test appear stable while leaving the program without the required ordering. Time passing is not a synchronization edge in the language memory model.

For hardware register access, follow the platform's required APIs and rules. Do not transfer an explanation about memory-mapped device access directly into a portable multithreaded C++ program, or the reverse.

How should you test and review atomic code?

Start with a written invariant and a small interleaving. Identify the operations that publish, acquire, update, and reclaim state. Review every ordinary shared access and every lifetime transition, not only the atomic line that looks sophisticated.

Use tools such as ThreadSanitizer where supported to find data races, and stress tests or model-checking approaches suitable for the algorithm. A clean test run is evidence, not a proof that all permitted executions are correct. Weak-ordering bugs can be rare and architecture-dependent.

QuestionEvidence required
Can the consumer read the payload?A valid synchronization and lifetime argument
Can an update be lost?Atomic read-modify-write or another protected transaction
Can memory be reclaimed?No remaining reader can access the old lifetime
Is the operation lock-free?Primitive support and complete algorithm progress analysis

Practice explaining the one-time publication example, then change it to repeated reuse of the same payload. Notice how resetting the flag and rewriting the data introduce new hazards. That follow-up is a strong test of whether you understood the original assumptions.

What should a concurrency regression test establish?

Use tests to exercise the protocol, but do not confuse repeated success with a proof. A stress test can expose races and lifetime mistakes; it cannot demonstrate that every execution permitted by the memory model is correct. State the synchronization argument separately. Identify the publishing operation, the observing operation, and the ordinary accesses ordered by that relationship. Add a race detector where supported, then test startup, shutdown, and reuse of the shared object. Those boundaries often violate assumptions that hold during steady-state operation.

Sources and Further Reading

Research checked September 24, 2026. The working draft evolves; use the standard version selected by the project for precise release-sequence and API rules.