Retiring Objects¶
Understanding how to retire objects for safe reclamation.
State Machine¶
Overview¶
When you remove an object from a lock-free data structure, you cannot immediately free it: other threads might still be accessing it. Instead, you retire it, handing DEBRA a raw pointer plus a destructor closure. The destructor runs once all threads have advanced past the retiring epoch.
The retire API¶
retire takes a type-erased pointer and a Destructor:
The destructor is a proc(p: pointer) {.nimcall.}. DEBRA does not interpret
the pointer; the destructor is responsible for any cleanup (calling
dealloc, GC_unref, custom finalizers, etc.).
Bridging ref T with retain and releaseDestructor¶
Lock-free data structures usually want to store Atomic[ptr T] field slots
because Atomic[ref T] falls back to a spinlock under arc/orc, silently
breaking lock-freedom. The debra/refptr module bridges Nim's GC-managed
ref types into raw pointers with explicit refcount tracking:
retain(obj: ref T) -> ptr TGC-refs the object and returns a raw pointer suitable forAtomic[ptr T]storage.releaseDestructor[T]() -> Destructorreturns a closure thatGC_unrefs aptr Tonce the epoch is safe.
Pair every retain with exactly one release (typically by handing
releaseDestructor[T]() to retire).
import debra
import debra/atomics
type
NodeObj = object
value: int
next: Atomic[ptr NodeObj]
Node = ref NodeObj
# Allocate and GC-pin a node; `node` is a raw `ptr NodeObj`.
let node = retain Node(value: 42)
# Later, after unlinking it from shared state:
let ready = retireReady(pinned)
discard ready.retire(cast[pointer](node), releaseDestructor[NodeObj]())
releaseDestructor[T]() returns a captureless nimcall function pointer:
each T instantiation produces one proc address that is reused across calls,
so handing it inline to retire does not allocate.
Self-Referential Types¶
For linked structures, use the ref Obj pattern with Atomic[ptr NodeObj]:
ptr is opaque to Nim's type checker, so the recursive shape resolves
naturally. There is no forward-declaration dance.
Basic Retirement¶
You must be pinned to retire:
Example source not mirrored
The examples/retire_single.nim source was part of the standalone
nim-debra repo and is not included in this frozen mirror. View it at
the upstream link below, or see the live
SMR guide for the current lockfree/smr/nebr API.
:material-file-code: View full source
Multiple Object Retirement¶
When retiring multiple objects in a single critical section, use
retireReadyFromRetired() to chain retirements:
Example source not mirrored
The examples/retire_multiple.nim source was part of the standalone
nim-debra repo and is not included in this frozen mirror. View it at
the upstream link below, or see the live
SMR guide for the current lockfree/smr/nebr API.
:material-file-code: View full source
Limbo Bags¶
Retired pointers (and their destructors) are stored in thread-local limbo bags:
- Each bag holds up to 64 entries
- Bags are chained together by epoch
- Reclamation walks bags from oldest to newest, invoking each destructor
Retirement Timing¶
Always unlink first, then retire:
# RIGHT - retire after unlinking
if head.compareExchangeStrong(oldHead, next, moRelease, moRelaxed):
let ready = retireReady(pinned)
discard ready.retire(cast[pointer](oldHead), releaseDestructor[NodeObj]())
# WRONG - retire before unlinking (unsafe!)
let ready = retireReady(pinned)
discard ready.retire(cast[pointer](oldHead), releaseDestructor[NodeObj]())
head.store(next, moRelease)
Best Practices¶
Do Retire Objects That:¶
- Were removed from shared data structures
- Are no longer reachable via shared pointers
- Might still be accessed by concurrent threads
Don't Retire Objects That:¶
- Are still reachable in the data structure
- Are local to the current thread (just let them go out of scope)
- Are static/global (they're never freed)
Next Steps¶
- Learn about reclamation
- Understand neutralization
- See integration examples