Getting started¶
Your first five minutes with lockfree: install, write a bounded SPSC
program, write an unbounded MPMC program, learn when to reach for the
standalone reclaimer.
If you are coming from lockfreequeues or nim-debra, the package was
renamed in v0.1.0; see
Migrations and
Migrations from nim-debra for the
import-path map.
Install¶
lockfree requires Nim >= 2.2.10 and is published on the Nimble
registry under the package name lockfree. The only runtime dependency
is typestates >= 0.12.0; the DEBRA safe-memory-reclamation substrate is
bundled in-tree as lockfree/smr/nebr (lifted from nim-debra), so it is
not a separate dependency.
Pin a version in your own .nimble file:
Use >= 0.1.0 only if you are willing to ride minor-version changes;
the public API is stable across patches.
Verify the install¶
# verify.nim
import options
import lockfree
var q = newSpscQueue[int, 16]()
discard q.push(42)
echo q.pop() # prints "Some(42)"
Compile and run with threads on:
The queues require --threads:on even when only one thread touches
them, because the implementation references Atomic[T] and
Thread[T]. This is the default in Nim 2.2+; explicit --threads:on
is a no-op there but still safe to set.
Your first bounded queue (SPSC)¶
The smallest useful program: one producer, one consumer, one bounded queue.
import options
import os
import lockfree
# Capacity 16, item type int.
var queue = newSpscQueue[int, 16]()
proc producerFunc() {.thread.} =
for i in 1 .. 8:
while not queue.push(i):
sleep(0) # push returns false when full
proc consumerFunc() {.thread.} =
var seen = 0
while seen < 8:
let item = queue.pop()
if item.isSome:
echo "got ", item.get
inc seen
else:
sleep(0)
var threads: array[2, Thread[void]]
createThread(threads[0], producerFunc)
createThread(threads[1], consumerFunc)
joinThreads(threads)
Both push and pop are wait-free on this SPSC arm. The output is
deterministic: got 1 through got 8.
See Queues / bounded-vyukov for the multi-producer / multi-consumer variants.
Your first unbounded queue (MPMC)¶
The unbounded MPMC arm uses the strict-LCRQ algorithm (Morrison & Afek 2013) with a DWCAS publish protocol and signal-driven epoch-based reclamation for safe segment freeing. The user-facing code does not have to know any of that — the queue handles its own reclaimer:
import options
import lockfree
import lockfree/endpoint
import lockfree/role_tags
# Unbounded MPMC: segment size 64, registry sized for 4 lifetime threads.
var queue = newUnboundedMpmcQueue[int, stEager, 64, 4]()
var producer = queue.getProducerHere() # registers this thread
producer.push(42)
var consumer = queue.getConsumerHere()
let item = consumer.pop() # some(42)
assert item == some(42)
The *Here templates are sugar for getProducer() + bindToThread()
when the caller is also the operating thread. For the
parent-obtains-then-hands-off-to-worker pattern, use the explicit
getProducer() + bindToThread() pair so registration lands on the
worker thread. See Typestates for the full lifecycle.
The MaxThreads parameter (the 4 above) counts the lifetime number
of distinct threads that will ever operate the queue, not the
concurrent count. Sizing matters: nebr has no per-thread unregister,
so each bindToThread() consumes a registry slot for the manager's
lifetime.
When to reach for nebr directly¶
nebr is exposed as a standalone reclaimer for users building their
own lock-free data structures. If you are only using the queues, you
never need to import nebr directly — the unbounded multi-consumer
queues manage their own private manager.
Reach for nebr when you are writing a lock-free stack, hash table,
skiplist, or other reclaimable structure. See
SMR / nebr for the manager lifecycle, the pin / unpin
protocol, and the retire / reclaim cadence.
# Standalone nebr (sketch — see smr/nebr.md for the full surface)
import lockfree/smr/nebr
var manager = newManager(maxThreads = 4)
manager.register()
# In your reclaimable critical section:
manager.pin()
let snapshot = atomicLoad(myAtomic)
# … use snapshot …
manager.unpin()
# When you retire a node:
manager.retire(node)
# Periodically (or at known safe points):
manager.reclaim()
Common pitfalls¶
"Item type is not lock-free safe"¶
Prior to v0.1.0, lockfreequeues rejected ref T, string, and
seq[T] payloads at compile time because slot copies could race with
refcount updates. v0.1.0 supports all three directly via the
ManagedRef and ManagedSlice
paths. The -d:allowNonLockFreeQueueItems flag is removed; if your
code set it, the compile-time guard is gone and the flag is now a
no-op (with a deprecation warning).
Forgetting --threads:on¶
If you see 'Atomic' is not declared or 'Thread' undeclared, add
--threads:on to your compile flags or set switch("threads", "on")
in config.nims. The flag is the default under Nim 2.2+ but does not
hurt to set explicitly.
Sizing MaxThreads too small¶
Each bindToThread() consumes a registry slot for the lifetime of the
manager. If you exhaust the registry, the call raises a
DebraRegistrationError. Size MaxThreads to the lifetime distinct
thread count, not the concurrent count.
Next steps¶
- Bounded vs unbounded — the cardinality chooser.
- ManagedRef / ManagedSlice —
ref T,string, andseq[T]payloads. - Typestates — the
Unbound → Bound → Closedlifecycle and thewithBoundEndpointRAII wrapper. - SMR / nebr — the standalone reclaimer.