Lock-Free Data Structures
Lock-free data structures let multiple threads share data without ever blocking each other with a mutex, using hardware atomic instructions instead. They trade a simpler mental model for brutal correctness bugs, which is why a trading system reaches for them only on the handful of paths where a lock's worst case is unacceptable.
Prerequisites: Mutexes, Spinlocks and Contention, The C++ Memory Model and Atomics, Memory Barriers and Ordering
A mutex solves concurrency by refusing to let two threads touch shared data at once. That is simple and almost always correct, but it has a specific failure mode: if the thread holding the lock gets paused — descheduled by the OS, stalled on a page fault, or just slow — every other thread waiting on that lock stalls too. On a market-data thread feeding a pricing engine, one unlucky pause can turn into a multi-millisecond gap in the book. Lock-free data structures avoid that by never letting one thread's pause block another's progress: at least one thread is always guaranteed to make forward progress, no matter what the others are doing.
The idea: hardware does the "locking" atomically
The building block is a single CPU instruction, compare-and-swap (CAS): "if this memory location still holds the value I expect, replace it with a new value — atomically, as one indivisible step." No other thread can see a half-finished update. A lock-free structure is built entirely out of operations like this instead of lock()/unlock().
The classic example is a lock-free stack (a Treiber stack). Pushing a node means: read the current top, point the new node at it, then CAS the top pointer from the old value to the new node. If another thread pushed in between, the CAS fails — because the "expected" value no longer matches — and the thread simply retries with the new top. Nobody blocks; the loser of the race just loops again.
struct Node { int value; Node* next; };
std::atomic<Node*> top{nullptr};
void push(Node* n) {
n->next = top.load(std::memory_order_relaxed);
while (!top.compare_exchange_weak(
n->next, n,
std::memory_order_release,
std::memory_order_relaxed)) {
// n->next was updated to the current top by the failed CAS; retry
}
}
Worked example: two threads racing to push
Stack starts empty (top = null). Thread A wants to push node X, thread B wants to push node Y, at nearly the same instant.
- A reads
top = null, setsX->next = null. - B reads
top = nulltoo (A hasn't written yet), setsY->next = null. - A's CAS runs first: "is
topstillnull? Yes."topbecomesX. A's push succeeds. - B's CAS runs: "is
topstillnull?" No — it'sXnow. CAS fails. B does not corrupt anything; it silently retries. - B reloads
top = X, setsY->next = X, retries the CAS: "istopstillX? Yes."topbecomesY. B's push succeeds.
Final state: top = Y → X → null. Both pushes landed, in some serial order, with zero mutex and zero thread ever blocked waiting on the other.
Lock-free means the system as a whole always makes progress, not that every individual operation is fast. A CAS-based push always succeeds within a bounded number of retries proportional to contention — no thread can be starved indefinitely by another thread simply holding something and stalling.
Where it shows up
Interviewers use "implement a thread-safe counter/stack/queue without a mutex" to check whether you understand CAS, the ABA problem (a value changes from A to B and back to A between your read and your CAS, so the check passes even though the structure changed underneath you — fixed with tagged pointers or hazard pointers), and memory ordering.
In a real system, lock-free structures live on the narrow set of paths where a mutex's worst case is unacceptable: the single-producer/single-consumer queue between a market-data thread and a strategy thread (see Single-Producer Single-Consumer Ring Buffers), atomic counters updated from an interrupt handler, and the hot path of an order gateway where one thread being descheduled must never stall another. Everywhere else — configuration, logging, anything off the hot path — a plain mutex is simpler, easier to reason about, and just as fast in practice.
Lock-free code is notoriously hard to get right and even harder to test: bugs are timing-dependent and can hide for months before a rare interleaving triggers them. Never write lock-free structures freehand for production; use a reviewed library (Boost.Lockfree, folly, moodycamel's queue) and reserve custom lock-free code for the specific bottleneck a profiler actually identified.
Related concepts
Practice in interviews
Further reading
- Herlihy & Shavit, The Art of Multiprocessor Programming (ch. 9-10)
- Preshing, An Introduction to Lock-Free Programming