Lock-freedom¶
lockfree is a library of lock-free concurrent data structures.
This page defines the progress hierarchy, explains what {.lockFree.}
buys you, and clarifies what lock-freedom does and does not guarantee.
Progress hierarchy¶
Three terms cover the standard progress guarantees for concurrent operations:
- Wait-free: every thread completes its operation in a bounded number of steps, regardless of what other threads do. The strongest guarantee; immune to scheduling decisions on other threads.
- Lock-free: at least one thread makes progress on every step. Individual threads may retry under contention, but the system as a whole never stalls.
- Blocking (mutex-based): a thread holding a lock blocks any other thread that needs the lock. A preempted holder stalls every waiter.
Wait-free is preferable when achievable; lock-free is what contended CAS loops give you. Both are strictly stronger than blocking, because neither can deadlock and neither stalls the system on a slow holder.
What lockfree guarantees per arm¶
| Type | Cardinality | Push | Pop |
|---|---|---|---|
BQueue |
SPSC | Wait-free | Wait-free |
BQueue |
SPMC | Wait-free | Lock-free |
BQueue |
MPSC | Lock-free | Wait-free |
BQueue |
MPMC | Lock-free | Lock-free |
Queue |
SPSC | Wait-free | Wait-free |
Queue |
SPMC | Wait-free | Lock-free |
Queue |
MPSC | Lock-free | Wait-free |
Queue |
MPMC (strict-LCRQ) | Lock-free | Lock-free |
The single-cardinality side (single producer, single consumer) of each arm is always wait-free because there is no contention on that side. The multi-cardinality side uses a CAS loop, which is lock-free but not wait-free under contention.
The {.lockFree.} pragma¶
lockfree ships a {.lockFree.} pragma you can apply to your own
procs. It is a documentation marker plus a static check that the
proc body does not call into anything that takes a lock:
- The pragma asserts the proc does not invoke
system/locksprimitives (acquire,release,withLock). - It does not analyse the transitive call graph — a proc marked
{.lockFree.}can still call into other procs that lock if you do not also mark them. - The pragma is for human auditing first, compile-time enforcement second. It does not measure cache behaviour, contention costs, or scheduling fairness.
import lockfree
proc enqueueOne(q: var BQueue[int, ccSingle, ccSingle, 16, 0, 0],
v: int) {.lockFree.} =
while not q.push(v):
discard # backoff omitted for clarity
If you call system.acquire from inside an {.lockFree.} proc, you
get a compile-time error. The pragma is opt-in; the queues themselves
are intrinsically lock-free without it.
What lock-freedom does NOT guarantee¶
A common confusion: lock-freedom is a progress property, not a latency property and not a correctness property.
- It does not bound latency. A lock-free CAS loop can retry many times under contention. If you need bounded latency, you need wait-free, not lock-free.
- It does not prevent priority inversion. A high-priority thread spinning in a CAS loop while a lower-priority thread holds the contested cell still wastes CPU.
- It does not prevent ABA. ABA is a separate correctness concern addressed via tagged pointers, sequence counters, or safe memory reclamation. The queues' bounded variants use the Vyukov sequence counter; the unbounded MPMC variant uses the DWCAS sequence-tag from LCRQ. See SMR.
- It does not free reclaimed memory safely on its own. Reclaiming memory that another thread is concurrently reading is unsafe unless there is an explicit reclamation protocol. The unbounded multi-consumer queues use nebr for this.
Further reading¶
- Maged M. Michael and Michael L. Scott, "Simple, Fast, and Practical Non-Blocking and Blocking Concurrent Queue Algorithms" (PODC 1996) — the M&S queue underlying the unbounded segment-chain SPSC / SPMC / MPSC arms.
- Adam Morrison and Yehuda Afek, "Fast Concurrent Queues for x86 Processors" (PPoPP 2013) — the LCRQ algorithm underlying the unbounded MPMC arm.
- Dmitry Vyukov's writings on the per-slot sequence counter protocol — the bounded multi-cardinality arms.