A strong answer
compare_exchange_weak(expected, desired) checks only whether the target atomic variable still holds the bit pattern expected (A), not whether the object at address A was destroyed and recycled in between.
Suppose Thread 1 wants to pop from a lock-free stack or queue where head points to node A, and A->next is B:
- Thread 1 reads
head(A) andnext(B), then gets preempted right before executinghead.compare_exchange_weak(A, B). - While Thread 1 is paused, Thread 2 pops
A, popsB, frees both, and pushes a brand-new node that the allocator happens to place at the exact same memory addressA, now withA->next = C(whileBis freed or reallocated elsewhere!). - Thread 1 wakes up and runs
head.compare_exchange_weak(A, B). Becauseheadonce again equals the addressA, the CAS succeeds and setshead = B—pointing the structure at a freed, dangling nodeB!
Even before the CAS runs, if Thread 2 frees and unmaps A before Thread 1 reads A->next, Thread 1 suffers an immediate use-after-free segfault. Two standard reclamation schemes prevent a node from being freed or recycled while any thread still holds a local reference to it:
- Hazard pointers: Each reader publishes the node pointer it is currently inspecting into a thread-local hazard slot before validating that
headhasn't changed; a retiring thread scans active hazard slots before freeing a node. - Epoch-based reclamation (EBR): Threads enter a critical epoch before accessing the structure and exit afterward; retired nodes are placed in the current epoch's limbo bag and freed only once all active threads have advanced past that epoch.
What the interviewer is listening for
- Traces a concrete 3-step ABA interleaving (
A -> BbecomesA -> CwhileBis freed, yet CAS onAsucceeds and installs danglingB). - Distinguishes the two problems: ABA pointer recycling (which version tags can detect if memory stays mapped in a fixed pool) and use-after-free on
A->nextdereference (which requires safe memory reclamation). - Contrasts hazard pointers (bounded memory, per-read memory barrier overhead) with epoch-based reclamation (very fast reads, but a single stalled thread delays reclamation for an entire epoch).
Common mistakes
- Calling
delete node;immediately after popping a node in a lock-free queue, which races with concurrent threads readingnode->next. - Assuming
std::shared_ptr<Node>is automatically lock-free and wait-free (standardshared_ptrcontrol block operations are atomic, but swapping a sharedshared_ptrhead withoutstd::atomic<std::shared_ptr<T>>is a data race, and manyatomic<shared_ptr>implementations use internal spinlocks).