Migration guide¶
lockfree v0.1.0 — umbrella consolidation
lockfree v0.1.0 is the consolidated umbrella of the former
lockfreequeues (v5.0.0) and nim-debra (v0.8.0) packages. If
you are coming from one of those packages, the per-package
migration paths are:
- From lockfreequeues v5 — covers the package rename, import path changes, and the static-thread-affinity endpoint API that shipped in v5.0.0.
- From nim-debra — covers the
DEBRA → nebr rename and the absorption into
lockfree/smr/nebr.
Concise upgrade notes for adopters moving forward across major versions. Each section lists removed/renamed symbols, behavioural changes, and the minimum code change to compile against the new version.
v2 → v3¶
v3.0.0 (2021-12-14) was a minor public-API release that did not remove or rename any queue types or constructors. The only user-visible API change adopters need to be aware of:
NoConsumersAvailableDefectandNoProducersAvailableDefectwere converted fromDefecttoCatchableError. Code that previouslydiscarded these (or relied on them aborting the process) can now catch them withtry ... except CatchableError. Existingdefect- catching code continues to compile; the type now inherits fromCatchableErrorrather thanDefect.
No source changes are required for upgrade. v3.0.0 also bumped the
supported Nim baseline to 1.6.0 and moved the changelog from
README.md to CHANGELOG.md.
v3 → v4¶
v4.0.0 (2026-04-30) reworked the bounded multi-cardinality slot
protocol and is a hard breaking change for anything that introspected
the internal field layout of Mupmuc, Mupsic, or Sipmuc.
Removed / replaced symbols¶
committed*,reservedHead*,reservedTail*,storage*fields onMupmuc,Mupsic,Sipmuc: removed. Replaced by a singlecells*: MPMCCellArrayN[N, T]field carrying per-slot Vyukov sequence counters.CommittedFlagsNtype: removed. Replaced bySlotSeqN,MPMCCellPayload, andMPMCCellArrayN.headandtailcursors on the bounded queue types: changed fromAtomic[int]toAtomic[uint64]. Code reading these via.load(...)needs an explicit cast or a local rename.
Behavioural changes¶
- Bulk
push(items)/pop(count)on the bounded multi-cardinality families is no longer atomic across the requested range. The new implementation performs a best-effort fill via a loop of singleton operations. Partial completion is still reported through the existingOption[Slice[int]]/Option[seq[T]]return types — the API surface is unchanged, but the intra-call atomicity guarantee is gone. - The bounded MPMC/SPMC/MPSC slot publication protocol switched to Vyukov per-slot sequence counters. This fixes a confirmed race that allowed two consumers to claim the same physical slot across generations (silent duplicate-item delivery + producer-vs-producer storage races). Existing call sites that only used the documented push/pop API observe the fix transparently.
Minimum code change¶
Adopters who only use the documented public surface (push, pop,
batch variants) need no source edits — the API shape is identical.
Adopters who reach into the bounded-queue field layout (e.g., for
diagnostic introspection or custom serialization) must migrate to the
new cells field and the uint64 cursor type.
v4 → v5¶
lockfreequeues 5.0.0 is a SemVer MAJOR release. The eight per-family queue type names from the 4.1.x line collapse to two unified generic types:
BQueue[T, ccProd, ccCons, N, P, C]— the bounded ring buffer (absorbsSipsic,Sipmuc,Mupsic,Mupmuc).Queue[T, ccProd, ccCons, ST, S, MaxThreads]— the unbounded linked-segment queue (absorbsUnboundedSipsic,UnboundedSipmuc,UnboundedMupsic,UnboundedMupmuc). The(ccSingle, ccSingle)arm absorbs the formerly standaloneUnboundedSipsicbody verbatim and stays debra-free.
The ccProd / ccCons parameters (ccSingle / ccMulti) select the
producer and consumer cardinality.
Zero-Breaking Compatibility Layer. To ensure zero friction for existing applications, lockfree provides a comprehensive backwards-compatibility shim layer. Existing code using import lockfreequeues or import debra continues to work with zero code modifications!
The compatibility layer (src/lockfreequeues.nim, src/debra.nim, src/lockfreequeues/*.nim, src/lockfree/compat/lockfreequeues.nim) exposes:
- Bounded aliases: Sipsic, Mupsic, Sipmuc, Mupmuc mapped to BQueue.
- Unbounded aliases: UnboundedSipsic, UnboundedMupsic, UnboundedSipmuc, UnboundedMupmuc mapped to Queue.
- Legacy smart constructors: newSipsicQueue, newMupsicQueue, newSipmucQueue, newMupmucQueue, etc.
- Automatic thread-affinity attachment on getProducer / getConsumer.
For new code, direct usage of BQueue and Queue with explicit cardinalities (ccSingle / ccMulti) or the family-named constructors (newSpscQueue, newMpmcQueue, newUnboundedMpmcQueue, …) is recommended.
Unbounded path dependency. The unbounded
Queuecardinalities other than(ccSingle, ccSingle)integratenim-debrafor epoch-based reclamation and requirenim-debra >= 0.8.0(the coordinated release wave:typestates 0.9.0→nim-debra 0.8.0→lockfreequeues 5.0.0). The boundedBQueuefamilies have no debra integration and migrate immediately. The unbounded SPSC arm (newUnboundedSpscQueue) is also debra-free.
What changed¶
- Removed public types:
Sipsic,Sipmuc,Mupsic,Mupmuc,UnboundedSipsic,UnboundedSipmuc,UnboundedMupsic,UnboundedMupmuc. Replaced byBQueue[T, ccProd, ccCons, N, P, C](bounded) andQueue[T, ccProd, ccCons, ST, S, MaxThreads](unbounded). - Removed public constructors:
initSipsic,initSipmuc,initMupsic,initMupmuc,newUnboundedSipsic,newUnboundedSipmuc,newUnboundedMupsic,newUnboundedMupmuc. Replaced by the two generic constructorsnewBQueue[T, ...]()(bounded) andnewQueue[T, ...]()(unbounded), plus the family-named wrappersnewSpscQueue/newMpscQueue/newSpmcQueue/newMpmcQueue(bounded) andnewUnboundedSpscQueue/newUnboundedSpmcQueue/newUnboundedMpscQueue/newUnboundedMpmcQueue(unbounded). DeallocationStrategyis now a static type parameterSTon the unboundedQueue. The runtimestrategy:field on the legacyUnbounded*queues is removed; everyif self.strategy == Xcollapses towhen ST == X, andstManualvsstEagermonomorphize separately.STdefaults toDefaultDeallocationStrategyon the constructors. BoundedBQueuecarries noSTaxis.- Cardinality is now exposed as static phantoms
ccProd, ccConson bothBQueueandQueue(and is threaded throughThreadHandle[MaxThreads, CC]on the unbounded queue's debra fields and the producer/consumer views). - No defaults for
ccProd/ccCons. The unification's purpose is to make cardinality explicit at the call site; defaults would re-introduce the implicit-cardinality ambiguity it eliminates. (The family-named smart constructors pre-bind the cardinality for you.) Queueis non-copyable. The unboundedQueueowns a heapptr Segmentchain and, for the debra-integrated cardinalities, aptr DebraManager; its=copyhook is a compile-time error. Move it, or share it byptr/varparameter into worker threads.BQueueremains copyable (it owns only inline slot storage).
Migration table¶
Every removed public symbol and its unified 5.0.0 replacement. This table is the authoritative reference for mechanical sed of an existing 4.1.x codebase.
Type declarations¶
Bounded families map to BQueue[T, ccProd, ccCons, N, P, C]; unbounded
families map to Queue[T, ccProd, ccCons, ST, S, MaxThreads]. For the
bounded families the per-side registry capacities P (producers) and C
(consumers) are 0 on the single-cardinality side.
| Before (4.1.x) | After (5.0.0) |
|---|---|
type X = Sipsic[N, T] |
type X = BQueue[T, ccSingle, ccSingle, N, 0, 0] |
type X = Sipmuc[N, C, T] |
type X = BQueue[T, ccSingle, ccMulti, N, 0, C] |
type X = Mupsic[N, P, T] |
type X = BQueue[T, ccMulti, ccSingle, N, P, 0] |
type X = Mupmuc[N, P, C, T] |
type X = BQueue[T, ccMulti, ccMulti, N, P, C] |
type X = UnboundedSipsic[S, T] |
type X = Queue[T, ccSingle, ccSingle, stEager, S, MaxThreads] |
type X = UnboundedSipmuc[S, T, MaxThreads] |
type X = Queue[T, ccSingle, ccMulti, stEager, S, MaxThreads] |
type X = UnboundedMupsic[S, T, MaxThreads] |
type X = Queue[T, ccMulti, ccSingle, stEager, S, MaxThreads] |
type X = UnboundedMupmuc[S, T, MaxThreads] |
type X = Queue[T, ccMulti, ccMulti, stEager, S, MaxThreads] |
Note the unbounded
UnboundedSipsic[S, T]gains aMaxThreadsparameter under the unifiedQueuetype even though the absorbed(ccSingle, ccSingle)arm is debra-free and never touches the registry. Pass any positiveMaxThreads(the family-namednewUnboundedSpscQueuesmart constructor makes this explicit).
Constructor call sites¶
The two generic constructors are newBQueue (bounded) and newQueue
(unbounded). The family-named smart constructors below pre-bind the
cardinality and are the recommended, lowest-churn target.
| Before (4.1.x) | After (5.0.0) |
|---|---|
var q = initSipsic[N, T]() |
var q = newSpscQueue[T, N]() |
var q = initSipmuc[N, C, T]() |
var q = newSpmcQueue[T, N, C]() |
var q = initMupsic[N, P, T]() |
var q = newMpscQueue[T, N, P]() |
var q = initMupmuc[N, P, C, T]() |
var q = newMpmcQueue[T, N, P, C]() |
var q = newUnboundedSipsic[S, T]() |
var q = newUnboundedSpscQueue[T, stEager, S, MaxThreads]() |
var q = newUnboundedSipmuc[S, T, MaxThreads](addr m) |
var q = newUnboundedSpmcQueue[T, stEager, S, MaxThreads](addr m) |
var q = newUnboundedMupsic[S, T, MaxThreads](addr m, h) |
var q = newUnboundedMpscQueue[T, stEager, S, MaxThreads](addr m, h) |
var q = newUnboundedMupsic[S, T, MaxThreads](addr m, h, Manual) |
var q = newUnboundedMpscQueue[T, stManual, S, MaxThreads](addr m, h) |
var q = newUnboundedMupmuc[S, T, MaxThreads](addr m) |
var q = newUnboundedMpmcQueue[T, stEager, S, MaxThreads](addr m) |
Equivalently, write the generic constructors directly:
newBQueue[T, ccMulti, ccMulti, N, P, C]()for the bounded shapes andnewQueue(Queue[T, ccMulti, ccMulti, stEager, S, MaxThreads], addr m)for the unbounded borrow forms. The family-named wrappers expand to exactly these.In v5,
STdefaults toDefaultDeallocationStrategy(which isstEagerwhen GC is enabled,stManualunder--mm:none). The tables above showstEagerexplicitly for fidelity to the v4 default; in new code you may omitSTfrom smart-constructor calls and let the default apply (e.g.,newUnboundedSpscQueue[T, S, MaxThreads]()).The runtime
strategy:argument that previously sat onnewUnboundedMupsic/newUnboundedSipmuc/newUnboundedMupmucis gone. To selectstManualvsstEager, write the desired strategy directly in theSTgeneric position (e.g., thestManualrow above). Each unbounded family also exposes a manager-borrow overload takingaddr managerand an auto-create overload taking no manager argument (it allocates a privateDebraManager).
Worked examples¶
The migration below shows each legacy family translated into its v5.0.0 shape, including a typical use that exercises the constructor and one push/pop.
Bounded — Mupsic (multi-producer / single-consumer)¶
import options
import lockfreequeues
# Before (4.1.x)
# var q = initMupsic[16, 4, int]()
# After (5.0.0): newMpscQueue[T, N, P] — N capacity, P producer slots.
var q = newMpscQueue[int, 16, 4]()
var p = q.getProducer()
discard p.push(42)
let v = q.pop()
assert v == some(42)
Bounded — Sipmuc (single-producer / multi-consumer)¶
import lockfreequeues
# Before
# var q = initSipmuc[16, 4, int]()
# After: newSpmcQueue[T, N, C] — N capacity, C consumer slots.
var q = newSpmcQueue[int, 16, 4]()
Bounded — Mupmuc (multi-producer / multi-consumer)¶
import lockfreequeues
# Before
# var q = initMupmuc[16, 4, 4, int]()
# After: newMpmcQueue[T, N, P, C].
var q = newMpmcQueue[int, 16, 4, 4]()
Bounded — Sipsic (single-producer / single-consumer)¶
import options
import lockfreequeues
# Before
# var q = initSipsic[16, int]()
# After: newSpscQueue[T, N]. Single-cardinality sides push/pop directly.
var q = newSpscQueue[int, 16]()
discard q.push(42)
let v = q.pop()
assert v == some(42)
Unbounded — UnboundedMupsic¶
The unbounded families take [T, ST, S, MaxThreads] (deallocation
strategy, segment size, debra registry capacity). They expose three
overloads: auto-create (no manager argument — allocates a private
DebraManager), manager-borrow (addr manager), and the manager-borrow
escape hatch that also accepts a pre-registered consumer handle.
import lockfreequeues
from debra import DebraManager, initDebraManager, registerThread
# Before (4.1.x)
# var mgr = initDebraManager[4]()
# let h = registerThread(mgr)
# var q = newUnboundedMupsic[16, int, 4](addr mgr, h)
# stEager was the runtime default; for Manual, the field was passed:
# var q = newUnboundedMupsic[16, int, 4](addr mgr, h, Manual)
# After (5.0.0): manager-borrow + pre-registered consumer handle.
# The handle's cardinality binds from the manager (ccSingle here).
var mgr = initDebraManager[4, debra.ccSingle]()
let h = registerThread(mgr)
var q = newUnboundedMpscQueue[int, stEager, 16, 4](addr mgr, h)
# For Manual: choose the ST generic at the call site.
# var q = newUnboundedMpscQueue[int, stManual, 16, 4](addr mgr, h)
Most code does not need to register the consumer handle up front. The
auto-create form lets the single consumer register itself with
bindConsumer() before its first pop, and each producer thread attaches
its own view:
import lockfreequeues
var q = newUnboundedMpscQueue[int, stEager, 16, 4]()
var c = q.bindConsumer() # v5.0.0: replaces v4.x attachConsumer() # on the consumer thread, before pop
var p = q.getProducer()
discard p.bindToThread() # v5.0.0: replaces v4.x attach() # on each producer thread, before push
p.push(42)
let v = q.pop()
Unbounded — UnboundedSipmuc¶
import lockfreequeues
from debra import DebraManager, initDebraManager
# Before
# var mgr = initDebraManager[4]()
# var q = newUnboundedSipmuc[16, int, 4](addr mgr)
# After: manager-borrow form.
var mgr = initDebraManager[4, debra.ccMulti]()
var q = newUnboundedSpmcQueue[int, stEager, 16, 4](addr mgr)
# Or auto-create (queue owns a private manager):
# var q = newUnboundedSpmcQueue[int, stEager, 16, 4]()
Unbounded — UnboundedMupmuc¶
import lockfreequeues
from debra import DebraManager, initDebraManager
# Before
# var mgr = initDebraManager[4]()
# var q = newUnboundedMupmuc[16, int, 4](addr mgr)
# After: manager-borrow form.
var mgr = initDebraManager[4, debra.ccMulti]()
var q = newUnboundedMpmcQueue[int, stEager, 16, 4](addr mgr)
# Or auto-create:
# var q = newUnboundedMpmcQueue[int, stEager, 16, 4]()
Unbounded — UnboundedSipsic (debra-free)¶
The standalone UnboundedSipsic type is gone, but its body is absorbed
into the (ccSingle, ccSingle) arm of Queue verbatim — same field
layout and committed-flag protocol, still debra-free (no attach()
needed). Use the newUnboundedSpscQueue smart constructor, which gains
the unified [T, ST, S, MaxThreads] parameter shape:
import options
import lockfreequeues
# Before
# var q = newUnboundedSipsic[16, int]()
# After: newUnboundedSpscQueue[T, ST, S, MaxThreads]. Unbounded queues
# push through a producer view; the single consumer pops on the queue.
var q = newUnboundedSpscQueue[int, stEager, 16, 4]()
var producer = q.getProducer()
producer.push(42)
let v = q.pop()
assert v == some(42)
Selection rules at a glance¶
When picking the generic parameters for BQueue (bounded) or Queue
(unbounded), the mapping is:
- bounded vs unbounded —
BQueuefor the four ring-buffer families (Sipsic,Sipmuc,Mupsic,Mupmuc);Queuefor the four linked-segment families (UnboundedSipsic,UnboundedSipmuc,UnboundedMupsic,UnboundedMupmuc). ccProd/ccCons— match the legacy family's first letter pair:Mu...->ccMulti,Si...->ccSingle. SoMupsicisccProd=ccMulti, ccCons=ccSingle;Sipmucis the inverse.N(BQueueonly) — ring-buffer capacity.P/C(BQueueonly) — producer / consumer registry slots; only meaningful for the multi-cardinality side. Use0on the single-cardinality side.ST(Queueonly) —stEagermatches the legacy default; passstManualexplicitly only when the legacynewUnbounded*(..., Manual)form was in use.S/MaxThreads(Queueonly) — unbounded segment size and debra thread-registry capacity. The absorbed-sipsic(ccSingle, ccSingle)arm is debra-free but still takes a positiveMaxThreadsfor parameter uniformity.
Both BQueue and Queue carry static validation guards
(validateBQueueParams / validateQueueParams) that fail at the
caller's instantiation site if the supplied parameter set is incoherent.
The compile error names the offending parameter, giving the same
instantiation-time feedback the legacy per-family types provided.
Dependency bumps¶
lockfree 0.1.0 requires:
typestates >= 0.12.0nim >= 2.2.10
The DEBRA safe-memory-reclamation substrate (formerly the external
nim-debra >= 0.8.0 dependency) is now bundled in-tree as
lockfree/smr/nebr, so it is no longer a separate dependency you need to
install. Reclamation is still only exercised when you instantiate the
DEBRA-integrated unbounded Queue cardinalities; bounded-only (BQueue)
users and the DEBRA-free unbounded SPSC arm do not engage the reclamation
path.