API Reference¶
Auto-generated API documentation from source code.
Main Module¶
debra
¶
nim-debra: DEBRA+ Safe Memory Reclamation
This library provides typestate-enforced epoch-based reclamation with signal-based neutralization for lock-free data structures.
registerThread raises ¶
proc registerThread(manager: var DebraManager[MaxThreads]): ThreadHandle[MaxThreads]
Register current thread with the DEBRA manager.
Must be called once per thread before any epoch operations. Raises DebraRegistrationError if max threads already registered.
Parameters
-
manager(var DebraManager[MaxThreads])
Returns
ThreadHandle[MaxThreads]
Raises
DebraRegistrationError
neutralizeStalled ¶
proc neutralizeStalled(manager: var DebraManager[MaxThreads]; epochsBeforeNeutralize: uint64 = 2): int
Signal all stalled threads. Returns number of signals sent.
Parameters
-
manager(var DebraManager[MaxThreads]) -
epochsBeforeNeutralize(uint64)
Returns
int
advance inline ¶
proc advance(manager: var DebraManager[MaxThreads])
Advance the global epoch.
Parameters
-
manager(var DebraManager[MaxThreads])
currentEpoch inline ¶
proc currentEpoch(manager: var DebraManager[MaxThreads]): uint64
Get current global epoch.
Parameters
-
manager(var DebraManager[MaxThreads])
Returns
uint64
bindClient inline ¶
proc bindClient(manager: var DebraManager[MaxThreads])
Register a client (e.g. a lock-free data structure) as bound to
this manager. Increments boundClients by 1.
Lock-free libraries built on nim-debra should call bindClient in
their constructor and unbindClient in their destructor. The
manager's destructor asserts the count is zero, so a non-zero
count at teardown means a client outlived its manager: the client
would continue calling into freed manager state.
Parameters
-
manager(var DebraManager[MaxThreads])
unbindClient inline ¶
proc unbindClient(manager: var DebraManager[MaxThreads])
Unregister a client previously bound via bindClient. Decrements
boundClients by 1. See bindClient for usage.
Asserts the previous count was positive: an underflow indicates an unbalanced unbind (e.g. double-destroy of a client) and is caught here with a precise stack trace, rather than later as a non-zero value seen by the manager destructor.
Parameters
-
manager(var DebraManager[MaxThreads])
clientCount inline ¶
proc clientCount(manager: var DebraManager[MaxThreads]): int
Number of clients currently bound to this manager. Relaxed load,
suitable for inspection and tests; not synchronized against
concurrent bindClient / unbindClient.
Parameters
-
manager(var DebraManager[MaxThreads])
Returns
int
Core Types¶
Type definitions for DEBRA+ manager and thread state.
types
¶
Core types for DEBRA+ implementation.
ThreadState
¶
type ThreadState[MaxThreads: static int] = object
Per-thread state tracked by the DEBRA manager.
Fields
-
currentBagptr LimboBag– Currently filling limbo bag. -
limboBagTailptr LimboBag– Tail of limbo bag list (oldest). -
advanceCounteruint64
DebraManager
¶
type DebraManager[MaxThreads: static int] = object
Coordinates epoch-based reclamation across threads.
ThreadHandle
¶
type ThreadHandle[MaxThreads: static int] = object
Handle for a registered thread. Required for pin/unpin.
Fields
-
idxint– Index into the threads array. -
managerptr DebraManager[MaxThreads]– Pointer to the manager.
DebraRegistrationError
¶
type DebraRegistrationError = object
Raised when thread registration fails (e.g., max threads reached).
initDebraManager ¶
proc initDebraManager(): DebraManager[MaxThreads]
Initialize a new DEBRA+ manager.
The global epoch starts at 1 (not 0) so that epoch 0 can represent "never observed" in thread state.
Returns
DebraManager[MaxThreads]
= ¶
proc =(manager: var DebraManager[MaxThreads])
Drain all per-thread limbo bags when the manager goes out of scope.
Worker threads must have joined before the manager is destroyed (the bag lists are owned by their respective slots without synchronization, so concurrent retire during destruction is undefined). Process-exit and end-of-scope cleanup is the typical caller.
Asserts via doAssert that no clients are still bound (boundClients
== 0). A non-zero count means a data structure built on this manager
(e.g. a lock-free queue) is being destroyed after its manager, which
is a programmer error: the client will continue to call into freed
manager state. The assertion fires in release builds because the
downstream UB is silent.
Without this, retire-but-not-yet-reclaimed objects (and the limbo bags that hold them) leak under ASAN at exit. With aggressive Manual strategies or short-lived managers, the leak is observable.
Parameters
-
manager(var DebraManager[MaxThreads])
Constants¶
Configuration constants for DEBRA+ algorithm.
constants
¶
Constants for DEBRA+ implementation.
DefaultMaxThreads
¶
const DefaultMaxThreads = 64
Default maximum number of threads that can be registered.
QuiescentSignal
¶
let QuiescentSignal = SIGUSR1
POSIX signal used for thread neutralization.
Limbo Bags¶
Data structures for thread-local retire queues.
limbo
¶
Limbo bag data structures for DEBRA+ retire queues.
A limbo bag holds up to 64 retired objects. Bags are linked together forming a thread-local retire queue.
Destructor
¶
type Destructor = proc (p: pointer) {.nimcall, raises: [].}
Destructor for retired objects. Marked raises: [] so the cleanup
paths in =destroy (which itself cannot raise) can call them
without an unlisted exception effect warning. EBR destructors
are reclamation hooks and should not raise; if cleanup can fail,
handle it inside the destructor.
RetiredObject
¶
type RetiredObject = object
Fields
-
datapointer -
destructorDestructor
LimboBag
¶
type LimboBag = object
Fields
-
objectsarray[LimboBagSize, RetiredObject] -
countint -
epochuint64 -
nextptr LimboBag
allocLimboBag ¶
proc allocLimboBag(): ptr LimboBag
freeLimboBag ¶
proc freeLimboBag(bag: ptr LimboBag)
Free a limbo bag (does NOT call destructors).
Parameters
-
bag(ptr LimboBag)
reclaimBag ¶
proc reclaimBag(bag: ptr LimboBag)
Call destructors for all objects in bag, then free bag.
Parameters
-
bag(ptr LimboBag)
Signal Handling¶
POSIX signal handling for neutralization protocol.
signal
¶
Signal handler for DEBRA+ thread neutralization.
When a thread is stalled (hasn't advanced its epoch), other threads can send SIGUSR1 to force it to unpin, allowing reclamation to proceed.
globalManagerPtr
¶
var globalManagerPtr: Atomic[pointer]
Set during manager initialization. Used by signal handler.
Stored as Atomic[pointer] (rather than a plain pointer) so the
publish path in setGlobalManager can use moRelease and the
consumer in the signal handler can use moAcquire. Without this,
the stride/header release stores would not synchronize with the
handler's reads through any defined happens-before edge — a
handler could observe the new pointer paired with stale stride or
header values from a previous manager.
Type: Atomic[pointer]
threadLocalManager
¶
var threadLocalManager: pointer
Manager pointer the calling thread is registered with. Stored as raw
pointer because the threadvar cannot carry the MaxThreads static
parameter. Compared by identity in reclaimStart(addr manager) to
reject the multi-manager hazard where a thread registered with manager
A would otherwise have its threadLocalIdx interpreted against
manager B's slot array.
Type: pointer
installSignalHandler ¶
proc installSignalHandler()
Install SIGUSR1 handler for DEBRA+ neutralization.
Safe to call multiple times - subsequent calls are no-ops. Called automatically during first DebraManager initialization.
Thread-safety: the signalHandlerInstalled flag is Atomic[bool],
acquire-loaded for the fast-path bail-out and release-stored after
a successful sigaction. Two threads may both observe the flag
false and both call sigaction; that is benign because
sigaction is thread-safe and re-arming the same handler is
idempotent at the OS level.
isSignalHandlerInstalled ¶
proc isSignalHandlerInstalled(): bool
forceReinstallSignalHandler ¶
proc forceReinstallSignalHandler()
Re-install the SIGUSR1 handler unconditionally, bypassing the
signalHandlerInstalled idempotency flag. Intended for test
isolation: when a sibling test installs a different SIGUSR1
handler via direct sigaction (e.g. the placeholder in
typestates/signal_handler), the OS-level handler is overwritten
even though signalHandlerInstalled remains true. Calling this
restores the real neutralizationHandler. Not part of the
library's public surface for normal use; prefer
installSignalHandler in production code.
setGlobalManager ¶
proc setGlobalManager(manager: ptr DebraManager[MaxThreads])
Set the global manager pointer for the signal handler and capture
the byte-stride information the handler needs to find each thread's slot.
Must be called once after manager initialization, before any
thread sends SIGUSR1. Safe to call repeatedly with the same
manager; passing a different manager replaces the captured
layout. Pass nil via the untyped overload below to clear the
global manager.
Race constraint: This routine MUST NOT race with signal
delivery. Either call it before any thread is registered, or fully
quiesce all threads (no in-flight or pending SIGUSR1) before
replacing the manager. The three atomic stores (header, stride,
pointer) are not a single transaction: a signal arriving partway
through could observe stride/header from one manager paired with
the pointer from another, and if the two managers were
instantiated with different MaxThreads their layouts differ.
The release/acquire edge below makes the intra-call publish
order consistent (a reader who sees the new pointer also sees the
new stride/header), but it does not make the call as a whole
atomic with respect to a concurrent reader, and it does not
protect against a reader pairing the new pointer with the old
stride that was just overwritten.
Parameters
-
manager(ptr DebraManager[MaxThreads])
setGlobalManager ¶
proc setGlobalManager(manager: pointer)
Untyped overload, retained so callers (including tests) can clear
the global manager by passing nil. Passing a non-nil raw pointer
is unsupported because the handler needs the per-type stride;
callers with a typed ptr DebraManager[N] should use the generic
overload above.
Race constraint: Same as the typed overload — must not race with signal delivery. Quiesce all registered threads before clearing the manager.
Parameters
-
manager(pointer)
Typestates¶
Signal Handler¶
Signal handler installation lifecycle.
signal_handler
¶
SignalHandler typestate.
Ensures signal handler is installed before DEBRA operations.
SignalHandlerContext
¶
type SignalHandlerContext = object
HandlerUninstalled
¶
type HandlerUninstalled = distinct SignalHandlerContext
HandlerInstalled
¶
type HandlerInstalled = distinct SignalHandlerContext
initSignalHandler ¶
proc initSignalHandler(): HandlerUninstalled
Create uninstalled signal handler context.
Returns
HandlerUninstalled
install transition ¶
proc install(h: HandlerUninstalled): HandlerInstalled
Install SIGUSR1 handler for DEBRA+ neutralization.
Parameters
-
h(HandlerUninstalled)
Returns
HandlerInstalled
isInstalled notATransition ¶
func isInstalled(h: HandlerInstalled): bool
Check if handler is installed.
Parameters
-
h(HandlerInstalled)
Returns
bool
Manager¶
Manager initialization and shutdown lifecycle.
manager
¶
DebraManager lifecycle typestate.
Ensures manager is initialized before use and properly shut down.
ManagerContext
¶
type ManagerContext[MaxThreads: static int] = object
Fields
-
managerptr DebraManager[MaxThreads]
ManagerUninitialized
¶
type ManagerUninitialized[MaxThreads: static int] = distinct ManagerContext[MaxThreads]
ManagerReady
¶
type ManagerReady[MaxThreads: static int] = distinct ManagerContext[MaxThreads]
ManagerShutdown
¶
type ManagerShutdown[MaxThreads: static int] = distinct ManagerContext[MaxThreads]
uninitializedManager ¶
proc uninitializedManager(mgr: ptr DebraManager[MaxThreads]): ManagerUninitialized[MaxThreads]
Wrap a manager pointer as uninitialized.
Parameters
-
mgr(ptr DebraManager[MaxThreads])
Returns
ManagerUninitialized[MaxThreads]
initialize transition ¶
proc initialize(m: sink ManagerUninitialized[MaxThreads]): ManagerReady[MaxThreads]
Initialize the manager. Sets epoch to 1, clears all state.
Parameters
-
m(sink ManagerUninitialized[MaxThreads])
Returns
ManagerReady[MaxThreads]
shutdown transition ¶
proc shutdown(m: sink ManagerReady[MaxThreads]): ManagerShutdown[MaxThreads]
Shutdown manager. Reclaims all remaining limbo bags.
Parameters
-
m(sink ManagerReady[MaxThreads])
Returns
ManagerShutdown[MaxThreads]
getManager ¶
func getManager(m: ManagerReady[MaxThreads]): ptr DebraManager[MaxThreads]
Get the underlying manager pointer.
Parameters
-
m(ManagerReady[MaxThreads])
Returns
ptr DebraManager[MaxThreads]
Registration¶
Thread registration lifecycle.
registration
¶
Registration typestate for thread registration.
Handles thread registration with the DEBRA manager, ensuring threads properly claim slots in the thread array using lock-free CAS operations.
RegistrationContext
¶
type RegistrationContext[MaxThreads: static int] = object
Fields
-
managerptr DebraManager[MaxThreads] -
idxint
Unregistered
¶
type Unregistered[MaxThreads: static int] = distinct RegistrationContext[MaxThreads]
Registered
¶
type Registered[MaxThreads: static int] = distinct RegistrationContext[MaxThreads]
RegistrationFull
¶
type RegistrationFull[MaxThreads: static int] = distinct RegistrationContext[MaxThreads]
unregistered ¶
proc unregistered(mgr: ptr DebraManager[MaxThreads]): Unregistered[MaxThreads]
Create unregistered context for a thread.
Parameters
-
mgr(ptr DebraManager[MaxThreads])
Returns
Unregistered[MaxThreads]
register transition ¶
proc register(u: sink Unregistered[MaxThreads]): RegisterResult[MaxThreads]
Try to register thread by claiming a slot. Returns Registered if successful,
RegistrationFull if all slots are taken.
Parameters
-
u(sink Unregistered[MaxThreads])
Returns
RegisterResult[MaxThreads]
idx notATransition ¶
func idx(r: Registered[MaxThreads]): int
Get the thread slot index.
Parameters
-
r(Registered[MaxThreads])
Returns
int
getHandle ¶
func getHandle(r: Registered[MaxThreads]): ThreadHandle[MaxThreads]
Extract ThreadHandle for use in pin/unpin operations.
Parameters
-
r(Registered[MaxThreads])
Returns
ThreadHandle[MaxThreads]
Thread Slot¶
Thread slot allocation and release.
slot
¶
ThreadSlot typestate for thread slot lifecycle.
Tracks the lifecycle of a thread slot in the DEBRA manager: - Free: Slot is available for claiming - Claiming: Thread is attempting to claim the slot - Active: Slot is actively in use by a thread - Draining: Thread is unregistering, draining limbo bags - Free: Slot released back to pool
SlotContext
¶
type SlotContext[MaxThreads: static int] = object
Fields
-
idxint -
managerptr DebraManager[MaxThreads]
Free
¶
type Free[MaxThreads: static int] = distinct SlotContext[MaxThreads]
Claiming
¶
type Claiming[MaxThreads: static int] = distinct SlotContext[MaxThreads]
Active
¶
type Active[MaxThreads: static int] = distinct SlotContext[MaxThreads]
Draining
¶
type Draining[MaxThreads: static int] = distinct SlotContext[MaxThreads]
freeSlot ¶
proc freeSlot(idx: int; mgr: ptr DebraManager[MaxThreads]): Free[MaxThreads]
Create a free slot context.
Parameters
-
idx(int) -
mgr(ptr DebraManager[MaxThreads])
Returns
Free[MaxThreads]
claim transition ¶
proc claim(f: sink Free[MaxThreads]): Claiming[MaxThreads]
Begin claiming this slot. Transition to Claiming state.
Parameters
-
f(sink Free[MaxThreads])
Returns
Claiming[MaxThreads]
activate transition ¶
proc activate(c: sink Claiming[MaxThreads]): Active[MaxThreads]
Complete slot claim. Transition to Active state.
This is where the slot becomes fully owned by a thread.
Parameters
-
c(sink Claiming[MaxThreads])
Returns
Active[MaxThreads]
drain transition ¶
proc drain(a: sink Active[MaxThreads]): Draining[MaxThreads]
Begin unregistration. Transition to Draining state.
Thread will drain its limbo bags before releasing the slot.
Parameters
-
a(sink Active[MaxThreads])
Returns
Draining[MaxThreads]
release transition ¶
proc release(d: sink Draining[MaxThreads]): Free[MaxThreads]
Release slot back to free pool. Transition back to Free state.
This completes the lifecycle, making the slot available for reuse.
Parameters
-
d(sink Draining[MaxThreads])
Returns
Free[MaxThreads]
idx notATransition ¶
func idx(s: Active[MaxThreads]): int
Get the slot index from Active state.
Parameters
-
s(Active[MaxThreads])
Returns
int
idx notATransition ¶
func idx(s: Draining[MaxThreads]): int
Get the slot index from Draining state.
Parameters
-
s(Draining[MaxThreads])
Returns
int
manager ¶
func manager(s: Active[MaxThreads]): ptr DebraManager[MaxThreads]
Get the manager pointer from Active state.
Parameters
-
s(Active[MaxThreads])
Returns
ptr DebraManager[MaxThreads]
manager ¶
func manager(s: Draining[MaxThreads]): ptr DebraManager[MaxThreads]
Get the manager pointer from Draining state.
Parameters
-
s(Draining[MaxThreads])
Returns
ptr DebraManager[MaxThreads]
Epoch Guard¶
Pin/unpin critical section lifecycle.
guard
¶
EpochGuard typestate for pin/unpin lifecycle.
Ensures threads properly enter/exit critical sections.
Pitfalls¶
- The
Unpinned->Pinned->Unpinned/Neutralizedtypestate sequence must be respected. Callingpinon a handle that is already pinned at the slot level (the manager'sthreads[idx].pinnedflag is true) leaves the slot inconsistent. UsewithPin(indebra/convenience) for the common path; reach for the typestate API only when you need finer control. - If
unpinreturnsNeutralized, the thread was signaled while inside the critical section. Callacknowledgebefore re-pinning. Skippingacknowledgewill leave the slot'sneutralizedflag set, and the nextpin/unpincycle will misreport state. Pinned[MT]carries the capturedepochfield; do not mutate the manager's global epoch under the assumption a particularPinnedvalue tracks it. Advance the manager epoch on the worker side, then re-pin to refresh.
See also¶
debra/convenience.withPin_ - the recommended high-level wrapper.debra/typestates/retire_ -RetireReadyconstructed fromPinned.
EpochGuardContext
¶
type EpochGuardContext[MaxThreads: static int] = object
Fields
-
handleThreadHandle[MaxThreads] -
epochuint64
Unpinned
¶
type Unpinned[MaxThreads: static int] = distinct EpochGuardContext[MaxThreads]
Pinned
¶
type Pinned[MaxThreads: static int] = distinct EpochGuardContext[MaxThreads]
Neutralized
¶
type Neutralized[MaxThreads: static int] = distinct EpochGuardContext[MaxThreads]
unpinned ¶
proc unpinned(handle: ThreadHandle[MaxThreads]): Unpinned[MaxThreads]
Create unpinned epoch guard context.
Parameters
-
handle(ThreadHandle[MaxThreads])
Returns
Unpinned[MaxThreads]
pin transition ¶
proc pin(u: sink Unpinned[MaxThreads]): Pinned[MaxThreads]
Enter critical section. Blocks reclamation of current epoch.
Parameters
-
u(sink Unpinned[MaxThreads])
Returns
Pinned[MaxThreads]
unpin transition ¶
proc unpin(p: sink Pinned[MaxThreads]): UnpinResult[MaxThreads]
Leave critical section. Returns Neutralized if signaled.
Parameters
-
p(sink Pinned[MaxThreads])
Returns
UnpinResult[MaxThreads]
acknowledge transition ¶
proc acknowledge(n: sink Neutralized[MaxThreads]): Unpinned[MaxThreads]
Acknowledge neutralization. Required before re-pinning.
See also: unpin (returns Neutralized when signaled), pin.
Parameters
-
n(sink Neutralized[MaxThreads])
Returns
Unpinned[MaxThreads]
epoch notATransition ¶
func epoch(p: Pinned[MaxThreads]): uint64
Get the epoch this thread is pinned at.
Parameters
-
p(Pinned[MaxThreads])
Returns
uint64
handle notATransition ¶
func handle(p: Pinned[MaxThreads]): ThreadHandle[MaxThreads]
Get the thread handle.
Parameters
-
p(Pinned[MaxThreads])
Returns
ThreadHandle[MaxThreads]
Retire¶
Object retirement to limbo bags.
retire
¶
Retire typestate for adding objects to limbo bags.
Must be pinned to retire objects.
Pitfalls¶
retireis sink-form: it consumesRetireReady[MT]and returnsRetired[MT]. To retire multiple objects in the same pinned epoch, convert back withretireReadyFromRetired. Thevar-form wrappers indebra/convenience(retire,retireBatch) handle this for you.- The pointer/destructor pair handed to
retire(p, dtor)is captured into the limbo bag immediately. The destructor closure must remain valid until reclamation runs (Destructoris animcallproc, so a top-level proc orreleaseDestructor[T]()result is fine). - Retiring the same pointer twice from different threads (or the same
thread across multiple
pincycles) double-frees on reclamation. The library does not deduplicate.
See also¶
debra/convenience.withPin_ +it.retire(...)- the recommended path.debra/typestates/reclaim_ - eventual reclamation after epoch safety.
RetireContext
¶
type RetireContext[MaxThreads: static int] = object
Fields
-
handleThreadHandle[MaxThreads] -
epochuint64
RetireReady
¶
type RetireReady[MaxThreads: static int] = distinct RetireContext[MaxThreads]
Retired
¶
type Retired[MaxThreads: static int] = distinct RetireContext[MaxThreads]
retireReady ¶
proc retireReady(p: Pinned[MaxThreads]): RetireReady[MaxThreads]
Create retire context from pinned state.
Parameters
-
p(Pinned[MaxThreads])
Returns
RetireReady[MaxThreads]
retireReadyFromRetired ¶
proc retireReadyFromRetired(r: sink Retired[MaxThreads]): RetireReady[MaxThreads]
Get back to RetireReady after retiring (for multiple retires).
Parameters
-
r(sink Retired[MaxThreads])
Returns
RetireReady[MaxThreads]
pinnedFromRetired ¶
proc pinnedFromRetired(r: sink Retired[MaxThreads]): Pinned[MaxThreads]
Return a Pinned[MaxThreads] reconstructed from a Retired[MaxThreads]
value, allowing the caller to keep working in the same pinned epoch (e.g. interleaving reads with further retires) instead of unpinning immediately after a retire.
Symmetric to retireReadyFromRetired: same underlying
RetireContext[MaxThreads], projected to the Pinned typestate via
the EpochGuardContext[MaxThreads] shape (handle + epoch). The slot's
pinned flag is unchanged; this is a typestate rebrand, not a
re-pin.
See also: retireReadyFromRetired, retire, debra/typestates/guard.pin_.
Parameters
-
r(sink Retired[MaxThreads])
Returns
Pinned[MaxThreads]
retire transition ¶
proc retire(r: sink RetireReady[MaxThreads]; p: pointer; destructor: Destructor): Retired[MaxThreads]
Retire a raw pointer for epoch-based reclamation.
The destructor will be called when the epoch becomes safe. Use for manually-managed memory (ptr types, alloc/dealloc, etc.)
Parameters
-
r(sink RetireReady[MaxThreads]) -
p(pointer) -
destructor(Destructor)
Returns
Retired[MaxThreads]
handle ¶
func handle(r: RetireReady[MaxThreads]): ThreadHandle[MaxThreads]
Parameters
-
r(RetireReady[MaxThreads])
Returns
ThreadHandle[MaxThreads]
Reclamation¶
Safe memory reclamation from limbo bags.
reclaim
¶
Reclaim typestate for safe memory reclamation.
Walks the calling thread's limbo bags and reclaims objects retired before
safeEpoch. Each thread reclaims only its own retired objects; cross-thread
reclamation is not supported because the limbo bag list is mutated by the
owning thread (via retire) without synchronization.
Per-thread reclamation¶
tryReclaim walks the calling thread's slot only. The slot is identified by
the ThreadHandle passed to reclaimStart(handle), or by the thread-local
threadLocalIdx set during registerThread for the legacy
reclaimStart(addr manager) form.
A thread that retires but never calls tryReclaim will accumulate limbo
bags that no other thread can reclaim on its behalf. If such a thread stalls
or exits without draining its bags, those objects are leaked. The
neutralizeStalled mechanism handles stalled-but-still-running threads (by
forcing them to unpin so the global epoch can advance); it does not free
their retired objects.
Pitfalls¶
- The
ReclaimStart->EpochsLoaded->ReclaimReady/ReclaimBlockedchain must be honored.ReclaimBlockedmeans the global epoch has not advanced far enough for any retired object to be safe; this is normal, not an error.manager.advance()increments the global epoch. - Reclamation does not require pinning. The walker only inspects per-thread
epoch stamps. Calling reclaim from a worker that is currently
Pinnedon the same manager is safe but will see its own thread as a constraint onsafeEpoch. tryReclaimisnotATransitionbecauseReclaimReadyis the terminal state of theReclaimContexttypestate and the count return value is what the caller cares about.reclaimStart(addr manager)infers the slot from the thread-localthreadLocalRegistered/threadLocalIdxpair, additionally guarded bythreadLocalManagerso a thread registered with manager A cannot accidentally walk manager B's slot list. If the calling thread has not been registered withmanager, the returned context carriesidx = -1andtryReclaimshort-circuits to 0 reclaimed (rather than silently walking slot 0's bag list and racing with its owner). Prefer the handle formreclaimStart(handle)regardless: it is explicit and needs no thread-local lookup.
See also¶
debra/convenience.reclaimNow_ - the one-shot wrapper.debra/typestates/retire_ - puts objects into limbo bags.
ReclaimContext
¶
type ReclaimContext[MaxThreads: static int] = object
Fields
-
managerptr DebraManager[MaxThreads] -
idxint– Slot index of the calling thread; bag walk targets this slot only. -
globalEpochuint64 -
safeEpochuint64
ReclaimStart
¶
type ReclaimStart[MaxThreads: static int] = distinct ReclaimContext[MaxThreads]
EpochsLoaded
¶
type EpochsLoaded[MaxThreads: static int] = distinct ReclaimContext[MaxThreads]
ReclaimReady
¶
type ReclaimReady[MaxThreads: static int] = distinct ReclaimContext[MaxThreads]
ReclaimBlocked
¶
type ReclaimBlocked[MaxThreads: static int] = distinct ReclaimContext[MaxThreads]
reclaimStart ¶
proc reclaimStart(handle: ThreadHandle[MaxThreads]): ReclaimStart[MaxThreads]
Begin reclamation attempt for the calling thread's own retired objects.
handle identifies the slot whose limbo bag list will be walked. Pass the
handle returned by registerThread from the same thread that retired the
objects. Each thread reclaims its own bags; this is the only safe pattern
without per-list synchronization.
See also: debra/convenience.reclaimNow_ for the one-shot wrapper.
Parameters
-
handle(ThreadHandle[MaxThreads])
Returns
ReclaimStart[MaxThreads]
reclaimStart ¶
proc reclaimStart(mgr: ptr DebraManager[MaxThreads]): ReclaimStart[MaxThreads]
Legacy entry point that infers the calling thread's slot from the
thread-local threadLocalIdx set by registerThread. Prefer the
reclaimStart(handle) overload.
If the calling thread has not been registered with this (or any) manager,
the returned context carries idx = -1, so tryReclaim short-circuits to
0 reclaimed instead of mutating slot 0's bag list (which would race with
that slot's owner). threadLocalIdx defaults to 0 and cannot by itself
distinguish "registered at slot 0" from "never registered"; the
companion threadLocalRegistered flag is the explicit registered-bit.
In multi-manager scenarios threadLocalIdx and threadLocalRegistered
describe the slot the calling thread holds with whichever manager it
last registered with. Calling reclaimStart(addr managerB) from a
thread registered with managerA would otherwise reuse A's slot index
against B's bag list, racing with B's actual slot owner. The companion
threadLocalManager pointer is therefore compared by identity here:
on mismatch the context is returned with idx = -1 and tryReclaim
short-circuits to 0.
See also: debra/convenience.reclaimNow_ for the one-shot wrapper.
Parameters
-
mgr(ptr DebraManager[MaxThreads])
Returns
ReclaimStart[MaxThreads]
loadEpochs transition ¶
proc loadEpochs(s: ReclaimStart[MaxThreads]): EpochsLoaded[MaxThreads]
Load global epoch and compute minimum epoch across pinned threads.
Subscription read: an SC load of globalEpoch participates in the C11
SC total order S, pairing with the SC RMW in pin to give EBR its
StoreLoad ordering across threads. Without this, a reclaimer can read
another thread's pinned=false even when that thread's pin RMW has
already been issued, then proceed to free an object the still-pinning
thread is about to read. The SC load ensures that for every concurrently
pinning thread T, either (a) T's pin RMW is visible here (we observe
pinned=true), or (b) T's subsequent load of the protected pointer
happens after our prior writes — so T cannot have observed a still-live
pointer to an object we are about to free.
The SC RMW used to be issued against manager.globalEpoch
(globalEpoch.fetchAdd(0)), but globalEpoch is a hot shared cache
line and every reclaimer-side subscription bounced it across cores.
Three properties were needed for the subscription handshake to be
correct under TSAN:
-
Hardware StoreLoad barrier. A plain SC load is too weak — on x86 it lowers to a bare
movand loses themfencethat an SC fence would provide. An SC RMW lowers to alock-prefixed instruction on x86 and anldaxr/stlxrseq-cst loop on ARM, both of which are full StoreLoad barriers. -
Participation in C11's SC total order S. The SC loads on each thread's
pinnedflag (below) are in S. For the proof of EBR safety to go through, the subscription point itself must be in S so that pin RMWs published before our subscription are visible. C11 §29.3 makes S a single total order across every SC op, regardless of which atomic location it touches. -
TSAN vector-clock modelling. Standalone SC fences are not modelled by TSAN (see
compiler-rt/lib/tsan/rtl/tsan_interface_atomic.cppOpFence::Atomic->// FIXME: not implemented.). SC RMWs are modelled correctly on every atomic location, so the analyser sees the synchronisation.
Properties (1) and (3) are properties of the instruction, not the
operand location; property (2) is location-independent by C11
construction. So a stack-local Atomic[uint64] gives us the same
subscription point with no cross-thread cache traffic.
Parameters
-
s(ReclaimStart[MaxThreads])
Returns
EpochsLoaded[MaxThreads]
safeEpoch notATransition ¶
func safeEpoch(e: EpochsLoaded[MaxThreads]): uint64
checkSafe transition ¶
proc checkSafe(e: EpochsLoaded[MaxThreads]): ReclaimCheck[MaxThreads]
Check if any epochs are safe to reclaim.
Parameters
-
e(EpochsLoaded[MaxThreads])
Returns
ReclaimCheck[MaxThreads]
tryReclaim notATransition ¶
proc tryReclaim(r: ReclaimReady[MaxThreads]): int
Reclaim eligible objects from the calling thread's own limbo bag list.
Returns the count of objects reclaimed. Walks only manager.threads[idx]
where idx was captured into the ReclaimContext by reclaimStart.
Other threads' bags are untouched: they reclaim their own.
Cross-thread reclamation would race with the owner thread's retire
mutations on currentBag and limboBagTail; those fields have no
synchronization and are owned by the registered thread.
Parameters
-
r(ReclaimReady[MaxThreads])
Returns
int
Neutralization¶
Thread neutralization protocol.
neutralize
¶
Neutralize typestate for DEBRA+ neutralization signaling.
Ensures proper sequence: ScanStart -> Scanning -> ScanComplete
When the epoch needs to advance, scan all threads and send SIGUSR1 to pinned threads that are stalled (behind globalEpoch by threshold).
NeutralizeContext
¶
type NeutralizeContext[MaxThreads: static int] = object
Fields
-
managerptr DebraManager[MaxThreads] -
globalEpochuint64 -
thresholduint64 -
signalsSentint
ScanStart
¶
type ScanStart[MaxThreads: static int] = distinct NeutralizeContext[MaxThreads]
Scanning
¶
type Scanning[MaxThreads: static int] = distinct NeutralizeContext[MaxThreads]
ScanComplete
¶
type ScanComplete[MaxThreads: static int] = distinct NeutralizeContext[MaxThreads]
scanStart ¶
proc scanStart(mgr: ptr DebraManager[MaxThreads]): ScanStart[MaxThreads]
Begin neutralization scan.
Parameters
-
mgr(ptr DebraManager[MaxThreads])
Returns
ScanStart[MaxThreads]
loadEpoch transition ¶
proc loadEpoch(s: ScanStart[MaxThreads]; epochsBeforeNeutralize: uint64 = 2): Scanning[MaxThreads]
Load global epoch and compute threshold for stalled threads.
Threads with epoch < (globalEpoch - epochsBeforeNeutralize) get signaled.
Parameters
-
s(ScanStart[MaxThreads]) -
epochsBeforeNeutralize(uint64)
Returns
Scanning[MaxThreads]
globalEpoch notATransition ¶
func globalEpoch(s: Scanning[MaxThreads]): uint64
Get the global epoch loaded during scan start.
Parameters
-
s(Scanning[MaxThreads])
Returns
uint64
threshold notATransition ¶
func threshold(s: Scanning[MaxThreads]): uint64
Get the epoch threshold for neutralization.
Parameters
-
s(Scanning[MaxThreads])
Returns
uint64
scanAndSignal transition ¶
proc scanAndSignal(s: Scanning[MaxThreads]): ScanComplete[MaxThreads]
Scan all registered threads and send SIGUSR1 to stalled pinned threads.
Returns count of signals sent.
Parameters
-
s(Scanning[MaxThreads])
Returns
ScanComplete[MaxThreads]
signalsSent notATransition ¶
func signalsSent(c: ScanComplete[MaxThreads]): int
Get number of signals sent during scan.
Parameters
-
c(ScanComplete[MaxThreads])
Returns
int
extractSignalCount notATransition ¶
func extractSignalCount(c: ScanComplete[MaxThreads]): int
Extract the count of signals sent. Terminal operation.
Parameters
-
c(ScanComplete[MaxThreads])
Returns
int
Epoch Advance¶
Global epoch advancement.
advance
¶
EpochAdvance typestate for advancing the global epoch.
Ensures atomic increment of the global epoch counter.
AdvanceContext
¶
type AdvanceContext[MaxThreads: static int] = object
Fields
-
managerptr DebraManager[MaxThreads] -
oldEpochuint64 -
newEpochuint64
Current
¶
type Current[MaxThreads: static int] = distinct AdvanceContext[MaxThreads]
Advancing
¶
type Advancing[MaxThreads: static int] = distinct AdvanceContext[MaxThreads]
Advanced
¶
type Advanced[MaxThreads: static int] = distinct AdvanceContext[MaxThreads]
advanceCurrent ¶
proc advanceCurrent(mgr: ptr DebraManager[MaxThreads]): Current[MaxThreads]
Create epoch advance context.
Parameters
-
mgr(ptr DebraManager[MaxThreads])
Returns
Current[MaxThreads]
advance transition ¶
proc advance(c: sink Current[MaxThreads]): Advancing[MaxThreads]
Begin advancing the global epoch.
Parameters
-
c(sink Current[MaxThreads])
Returns
Advancing[MaxThreads]
complete transition ¶
proc complete(a: sink Advancing[MaxThreads]): Advanced[MaxThreads]
Complete epoch advance by atomically incrementing globalEpoch.
Parameters
-
a(sink Advancing[MaxThreads])
Returns
Advanced[MaxThreads]
newEpoch notATransition ¶
func newEpoch(a: Advanced[MaxThreads]): uint64
Get the new epoch value after advancement.
Parameters
-
a(Advanced[MaxThreads])
Returns
uint64
oldEpoch notATransition ¶
func oldEpoch(a: Advanced[MaxThreads]): uint64
Get the old epoch value before advancement.
Parameters
-
a(Advanced[MaxThreads])
Returns
uint64