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:
# examples/retire_single.nim
## Single object retirement: the minimal pin -> retire -> unpin flow.
##
## Uses the `Atomic[ptr T]` lock-free pattern: `retain` to GC-pin a `ref` and
## hand back a raw pointer, `releaseDestructor[T](../../examples)` to balance the retain at
## reclamation time.
import debra
type
NodeObj = object
value: int
Node = ref NodeObj
proc main() =
var manager = initDebraManager[4](../../examples)
setGlobalManager(addr manager)
let handle = registerThread(manager)
# Enter critical section
let pinned = unpinned(handle).pin()
# Retain a ref so it survives until DEBRA reclamation. `retain` returns a
# raw `ptr NodeObj` suitable for atomic storage.
let node = retain Node(value: 42)
echo "Created node with value: ", node.value
# Retire the node. The destructor (releaseDestructor) will GC_unref the
# underlying ref once the epoch is safe.
let ready = retireReady(pinned)
discard ready.retire(cast[pointer](../../examples/node), releaseDestructor[NodeObj](../../examples))
echo "Node retired for later reclamation"
# Exit critical section
discard pinned.unpin()
echo "Single retirement example completed"
when isMainModule:
main()
Multiple Object Retirement¶
When retiring multiple objects in a single critical section, use
retireReadyFromRetired() to chain retirements:
# examples/retire_multiple.nim
## Retire multiple objects within a single pinned epoch.
##
## Demonstrates chaining retires by threading `RetireReady` through
## `retireReadyFromRetired`, which is the lower-level form behind the
## `var RetireReady` overload used by `withPin` bodies.
import debra
type
NodeObj = object
value: int
Node = ref NodeObj
proc main() =
var manager = initDebraManager[4](../../examples)
setGlobalManager(addr manager)
let handle = registerThread(manager)
# Enter critical section
let pinned = unpinned(handle).pin()
# Retire multiple nodes in a single critical section. The `var`-form
# `retire` from `debra/convenience` rebuilds `RetireReady` for us; here we
# do it explicitly to show the typestate transition.
var ready = retireReady(pinned)
let dtor = releaseDestructor[NodeObj](../../examples)
for i in 1 .. 5:
let node = retain Node(value: i * 10)
echo "Retiring node with value: ", node.value
# `var`-form retire from debra/convenience: consumes and rebuilds `ready`
# in place. Equivalent to `let r = retire(move(ready), ...);
# ready = retireReadyFromRetired(r)`.
ready.retire(cast[pointer](../../examples/node), dtor)
echo "Retired 5 nodes"
# Rebuild the Pinned context from the last RetireReady so we can unpin.
let ctx = RetireContext[4](../../examples/ready)
let pinnedAgain =
Pinned[4](../../examples/EpochGuardContext[4](handle: ctx.handle, epoch: ctx.epoch))
discard pinnedAgain.unpin()
echo "Multiple retirement example completed"
when isMainModule:
main()
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