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 the object, marking it for later reclamation when safe.

The Managed Type

DEBRA uses the Managed[T] wrapper type to track objects that need epoch-based reclamation:

type
  NodeObj = object
    value: int
    next: Atomic[Managed[ref NodeObj]]
  Node = ref NodeObj

# Create a managed node - GC won't collect until retired
let node = managed Node(value: 42)

The managed() proc calls GC_ref internally, preventing Nim's garbage collector from freeing the object. Only DEBRA's reclamation process (via GC_unref) can free it.

Self-Referential Types

When a type references itself (like linked list nodes), use the ref Obj pattern:

# CORRECT - use ref Obj pattern for self-reference
type
  NodeObj = object
    value: int
    next: Atomic[Managed[ref NodeObj]]  # Self-reference works
  Node = ref NodeObj

# INCORRECT - ref object inline won't work
type
  Node = ref object
    value: int
    next: Atomic[Managed[Node]]  # Won't compile

This is a Nim limitation with generic distinct types and forward references.

Basic Retirement

You must be pinned to retire objects:

# examples/retire_single.nim
## Single object retirement example.

import debra
import std/atomics

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

  # Create a managed node
  let node = managed Node(value: 42)
  echo "Created node with value: ", node.value

  # Retire the node
  let ready = retireReady(pinned)
  discard ready.retire(node)
  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
## Multiple object retirement example.

import debra
import std/atomics

type
  NodeObj = object
    value: int
    next: Atomic[Managed[ref NodeObj]]
  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
  var ready = retireReady(pinned)

  for i in 1..5:
    let node = managed Node(value: i * 10)
    echo "Retiring node with value: ", node.value
    let retired = ready.retire(node)
    ready = retireReadyFromRetired(retired)

  echo "Retired 5 nodes"

  # Exit critical section
  discard pinned.unpin()

  echo "Multiple retirement example completed"

when isMainModule:
  main()

View full source

Accessing Managed Fields

The Managed[T] type provides transparent field access:

let node = managed Node(value: 42, name: "test")

# Direct field access via dot template
echo node.value  # 42
echo node.name   # "test"

# When you need the actual ref
doSomething(node.inner)

Limbo Bags

Retired objects are stored in thread-local limbo bags:

  • Each bag holds up to 64 objects
  • Bags are chained together by epoch
  • Reclamation walks bags from oldest to newest

Retirement Timing

Always unlink first, then retire:

# RIGHT - retire after unlinking
if head.compareExchange(oldHead, next, moRelease, moRelaxed):
  let ready = retireReady(pinned)
  discard ready.retire(oldHead)

# WRONG - retire before unlinking (unsafe!)
let ready = retireReady(pinned)
discard ready.retire(oldHead)
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