Skip to content

Retiring Objects

Understanding how to retire objects for safe reclamation.

State Machine

Retire Typestate

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:

proc retire(ready: RetireReady[N], p: pointer, dtor: Destructor): Retired[N]

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 T GC-refs the object and returns a raw pointer suitable for Atomic[ptr T] storage.
  • releaseDestructor[T]() -> Destructor returns a closure that GC_unrefs a ptr T once 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]:

type
  NodeObj = object
    value: int
    next: Atomic[ptr NodeObj]
  Node = ref 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()

View full source

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()

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