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:
ensure_runtime— idempotent one-shot init of the global fiber pool.spawn_task— fire-and-forget fiber for aFnOnce() + Send.parallel_for_each— fan-out / fan-in over an iterator of closures.
§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§
- Runtime
Gate - Marker returned by
ensure_runtimeso callers can prove the pool is alive without re-checking. Stored once inRUNTIME_GATEand copied freely thereafter. - Task
Handle - Opaque handle for a spawned task. Returned by
spawn_taskand consumed byjoin.
Functions§
- ensure_
runtime - Initializes the global
dtactruntime on first call; subsequent calls are O(1) and return the sameRuntimeGate. - join
- Blocks the calling thread (or yields the calling fiber) until the task
behind
handlefinishes. - parallel_
for_ each - Runs each closure in
taskson its own fiber and waits for all of them to finish before returning. Closures produce aTwhich is collected into the returnedVecin input order. - runtime_
gate - Returns the active runtime gate if the pool is initialized.
- spawn_
task - Spawns
fonto the fiber pool and returns a joinableTaskHandle.