Skip to main content

Module runtime

Module runtime 

Source
Expand description

Fiber-based task runtime built on dtact.

Replaces std::thread::spawn with lightweight fibers throughout the crate. parallel_for_each is the workhorse used by parallel::solver and ffi::async_bridge. Fiber-based task runtime built on dtact.

plan.md §4.3 mandates an async-fiber interface, and the review found every async path (ffi::async_bridge, parallel::solver) still using heavyweight std::thread::spawn. This module funnels all task dispatch through dtact’s lock-free fiber pool, exposing three primitives:

§Task-envelope memory strategy

Each spawned fiber needs a heap allocation to carry its closure across thread boundaries (the spawning thread owns the closure; a dtact worker thread will read and execute it). Routing every spawn through the global allocator (malloc / HeapAlloc) costs ~20–100 ns per task — enough to negate the scheduling advantage of fibers for small, rapid fan-outs.

§Lock-free pool (fast path)

When a closure fits in POOL_INLINE_CAPACITY bytes and has alignment ≤ 16, the closure is written into a pre-allocated PoolNode drawn from a global ABA-safe Treiber stack (POOL_HEAD). The worker returns the node to the stack after moving the closure out — one LOCK CMPXCHG8B instead of a malloc + free.

§ABA safety

A plain AtomicPtr Treiber stack suffers from ABA: if a node is popped, used for a task that completes and returns the node, all before the original thread’s CAS fires, the stale next pointer silently wins and can corrupt the list. We prevent this with a tagged pointer: POOL_HEAD is an AtomicU64 whose top 16 bits hold a 16-bit generation tag (bottom 48 bits = pointer, always ≤ 48 bits on x86-64 without LA57). Each successful CAS increments the tag; a stale observer always sees a different tag and retries. AtomicU64::new(0) is a stable const fn with no software-lock fallback on any 64-bit target.

§TaskEnvelope fallback (large / over-aligned closures)

Closures that exceed POOL_INLINE_CAPACITY or require alignment > 16 fall back to the monomorphised TaskEnvelope<F> + Box::into_raw path. These are rare in practice (typical captures: a few Arc / usize values, all ≤ 8-byte aligned).

§Shared trampoline

Both paths store the monomorphised invoke pointer at byte offset 0 (#[repr(C)] for TaskEnvelope, repr(C, align(16)) with word0 at offset 0 for PoolNode). The single task_trampoline reads those bytes as unsafe fn(*mut ()) and dispatches without knowing which path produced the pointer.

Structs§

RuntimeGate
Marker returned by ensure_runtime so callers can prove the pool is alive without re-checking. Stored once in RUNTIME_GATE and copied freely thereafter.
TaskHandle
Opaque handle for a spawned task. Returned by spawn_task and consumed by join.

Functions§

ensure_runtime
Initializes the global dtact runtime on first call; subsequent calls are O(1) and return the same RuntimeGate.
join
Blocks the calling thread (or yields the calling fiber) until the task behind handle finishes.
parallel_for_each
Runs each closure in tasks on its own fiber and waits for all of them to finish before returning. Closures produce a T which is collected into the returned Vec in input order.
runtime_gate
Returns the active runtime gate if the pool is initialized.
spawn_task
Spawns f onto the fiber pool and returns a joinable TaskHandle.