A strong answer
A strong candidate frames When does an atomic counter beat a mutex-guarded counter? around the exact memory layout, compiler semantics, and hardware execution model covered in std::atomic: Counters and Flags Without Mutexes. Compare fetch_add indivisibility with lock cost, flag signaling with exchange, and sharding for hot contended counters.
A shared total incremented by two threads with plain addition loses updates. Each thread loads, adds and stores, and the two sequences interleave so one store overwrites the other. Guarding the three steps with a lock fixes correctness but puts a lock round trip on every increment, with parking once contention rises. The trace above shows the lighter fix, where each increment is a single indivisible step and both survive.
An atomic object supports indivisible read modify write operations directly in hardware. A mutex guarded section serializes arbitrary code around shared data. Counters and flags need only the former, while queues and compound invariants still need the latter.
In a production interview, always connect the specification rule to its concrete runtime consequence: state what lives on the stack versus the heap or static storage segment, how the compiler or operating system validates alignments, lifetimes, and error codes, and what happens under adversarial or boundary inputs (such as zero-length buffers, integer overflow, concurrent mutation, or partial I/O). Backing your explanation with a small trace of register/memory state transitions immediately separates senior systems engineers from candidates who only memorized syntax.
What the interviewer is listening for
- Mastery of core invariant: Rewrite a mutex-guarded counter with atomic fetch_add.
- Mastery of core invariant: Compare atomic and mutex cost for contended increments.
- Mastery of core invariant: Signal between threads with atomic flags and exchanges.
- Hardware and ABI awareness: Explaining how sizes, alignments, calling conventions, or kernel boundaries behave on real 64-bit targets rather than relying on implementation-defined assumptions.
- Defensive engineering: Naming the exact compiler flags (
-Wall -Wextra -Werror), static analyzers, or runtime sanitizers (-fsanitize=address,undefined,thread) that verify correctness in CI.
Common mistakes
- Explaining only surface syntax without tracing the underlying memory addresses, ownership lifetime, or kernel/user-space state transitions.
- Assuming undefined behavior (such as signed overflow, uninitialized reads, strict-aliasing violations, or dangling pointers) will "just work" because it passed an unoptimized
-O0debug build. - Ignoring return codes, partial reads/writes, alignment padding, or cleanup paths when an allocation or syscall fails midway through a function.