Lock-Free Architecture, Hardware CAS & The ABA Problem
Master lock-free data structures, atomic Compare-And-Swap (CAS), compare_exchange vs weak loops, and mitigating the classic ABA memory hazard.
Traditional mutexes block threads via operating system sleep queues, introducing context switching latency (thousands of CPU cycles). In ultra-low-latency networking, audio processing, and high-frequency trading, developers build Lock-Free Data Structures.
A lock-free algorithm guarantees that regardless of thread contention, at least one thread in the system makes forward progress in a finite number of CPU instructions.
The core hardware instruction (LOCK CMPXCHG) enabling lock-free state transitions without OS mutexes.
1. The Compare-And-Swap (CAS) Primitive#
At the heart of every lock-free data structure is the Compare-And-Swap (CAS) CPU instruction:
In Rust atomics, this is exposed via compare_exchange and compare_exchange_weak:
123456789101112131415161718use std::sync::atomic::{AtomicU32, Ordering}; fn lock_free_add(atomic_val: &AtomicU32, delta: u32) { let mut current = atomic_val.load(Ordering::Relaxed); loop { let new = current + delta; // Attempt atomic transition from 'current' to 'new': match atomic_val.compare_exchange_weak( current, new, Ordering::AcqRel, // Memory ordering on success Ordering::Relaxed // Memory ordering on failure ) { Ok(_) => break, // Successfully updated without locks! Err(actual) => current = actual, // Spurious failure or contention: retry! } } }
compare_exchange_weak can fail spuriously due to cache line invalidation. In a retry loop, always use compare_exchange_weak for optimal hardware pipeline performance.2. Lock-Free Stack (Treiber Stack)#
A classic example of lock-free architecture is the Treiber Stack:
1234567891011121314151617181920212223242526272829303132333435363738use std::sync::atomic::{AtomicPtr, Ordering}; use std::ptr; struct Node<T> { data: T, next: *mut Node<T>, } pub struct LockFreeStack<T> { head: AtomicPtr<Node<T>>, } impl<T> LockFreeStack<T> { pub fn new() -> Self { LockFreeStack { head: AtomicPtr::new(ptr::null_mut()) } } pub fn push(&self, data: T) { let new_node = Box::into_raw(Box::new(Node { data, next: ptr::null_mut(), })); let mut current_head = self.head.load(Ordering::Relaxed); loop { unsafe { (*new_node).next = current_head; } match self.head.compare_exchange_weak( current_head, new_node, Ordering::Release, Ordering::Relaxed, ) { Ok(_) => break, Err(actual) => current_head = actual, } } } }
3. The Classic ABA Problem#
The ABA Problem is the primary hazard in lock-free memory reclamation:
Mitigating the ABA Problem#
- Generational Tagged Pointers: Pair pointer addresses with an integer version counter:
Even if the address is reused, the version increments (), causing CAS to fail safely.
- Hazard Pointers / Epoch-Based Reclamation: Libraries like
crossbeam-epochdelay memory deallocation until all concurrent threads have exited reading critical sections.
4. Concept Verification#
Test your understanding of lock-free architecture:
What is the ABA problem in lock-free memory management?
Why is compare_exchange_weak preferred over compare_exchange inside retry loops?