Skip to article
ALGORITHMICSConcurrency
Concurrency6 min read

Lock-Free Structures

Progress guarantees, and the memory-reclamation problem that makes them hard.


“Lock-free” does not mean “no locks in the source”. It is a precise claim about progress, and the precision is the point.

Progress guarantee

No guarantee at all.

thread A progressing
thread B may stall
thread C may stall

One thread suspended while holding the lock stops everyone. That includes being descheduled by the OS, hitting a page fault, or being killed.

Each level is strictly stronger and strictly harder to implement. Almost all real lock-free code stops at the middle rung, because wait-free versions of the same structure are typically several times slower when there is no contention — which is most of the time.

A lock-free stack

The classic, and short enough to read in full:

class LockFreeStack<T> {
private head = new Atomic<Node<T> | null>(null);
push(value: T) {
const node = new Node(value);
let current: Node<T> | null;
do {
current = this.head.load();
node.next = current; // point at what we saw
} while (!this.head.compareExchange(current, node)); // publish, or retry
}
pop(): T | undefined {
let current: Node<T> | null;
do {
current = this.head.load();
if (current === null) return undefined;
} while (!this.head.compareExchange(current, current.next));
return current.value;
}
}

The whole structure is one CAS loop. If somebody else changed head between the load and the exchange, we re-read and try again — and that retry only happened because they succeeded, which is exactly the lock-free guarantee.

Why this is harder than it looks

ABA. CAS cannot distinguish “unchanged” from “changed back”, and for pointers that is a correctness bug rather than a curiosity. Tagged pointers or double-width CAS.

Multi-word updates are impossible. CAS handles one word. A structure needing two pointers changed together — a doubly linked list, most balanced trees — has no straightforward lock-free version. Papers exist; they are long.

Correctness is not testable. These algorithms fail on interleavings you will not hit by running the tests a million times. Published lock-free algorithms have shipped with bugs found years later by model checkers.

The performance is not automatic

Under high contention, a CAS loop can be slower than a mutex. Every failed attempt is wasted work and a cache line bounced between cores. A mutex parks the loser, which consumes nothing.

Under low contention, lock-free wins — no syscall, no scheduler.

The honest summary: lock-free is about latency predictability, not throughput. If you need a bounded worst case, it is the tool. If you want the program to be faster on average, measure first, because it often is not.