pub type Result<T> = Result<T, Error>; ForkUnion is dead-simple to use!
There is no nested parallelism, exception handling, or “future promises”; they are banned.
A pool runs one dispatch at a time and refuses any other with the contended status, including one from inside its own callbacks.
The thread pool itself has a few core operations:
spawnto initialize worker threads, andfor_threadsto launch a blocking callback on all threads.
Higher-level APIs for index-addressable tasks are also available:
for_n- for individual evenly-sized tasks,for_n_dynamic- for individual unevenly-sized tasks,for_slices- for contiguous runs of evenly-sized tasks, one per worker.
For additional flow control and tuning, following helpers are available:
nap- to park workers on the CPU’s address monitor, waking with no syscall,sleep(microseconds)- to block workers in the kernel for long idle periods, with 0 waiting for the next dispatch,terminate- to kill the threads before the destructor is called,unsafe_for_threads- to broadcast a callback without blocking, returning a generation token,unsafe_join- to block until the completion of a broadcasted generation,is_complete- to poll a generation token for completion without blocking.
Every dispatch is identified by a generation token.
On caller_exclusive_k pools you can dispatch, overlap your own work, poll is_complete, and join - the classic poll-then-join pattern.
On caller_inclusive_k pools the calling thread owes one slice of the work, which only runs inside unsafe_join, so completion can’t be reached by polling alone.
The same rule shapes the RAII guard returned by for_threads and, in C++, the for_n family: the workers start at the guard’s construction on every pool, and on inclusive pools the caller’s own slice runs at join or destruction.
On caller_exclusive_k pools thread 0 may also fork from inside its own slice, at any depth: such a fork waits for every other slice of the one enclosing it, runs on all threads, and returns complete.
So unsafe_for_threads can hand a whole multi-step call to thread 0, which forks the rest of the pool as it goes, while the caller only polls a status word the call stores at its end.
That is how one manager thread keeps several exclusive pools busy with no queue and no extra thread.
A join on such a pool waits for whatever dispatch runs, since the call may end before its submitter stores the token.
In C++ it provides NUMA extensions.
That includes the colocated_pool analog of the flat_pool and the domain_allocator_t for allocating memory in a specific memory domain.
Those are out-of-the-box compatible with the higher-level APIs.
Most interestingly, for Big Data applications, a higher-level distributed_pool class addresses and balances the work across all compute domains.
domain_allocator_tis backed bylinux_numa_allocator_t,freebsd_numa_allocator_t, orwindows_numa_allocator_t.
To integrate into your Rust project, add the following lines to Cargo.toml:
[dependencies]
forkunion = "4.0.0-rc.1" # detect what the platform offers
forkunion = { version = "4.0.0-rc.1", features = ["portable"] } # STL thread pool only
forkunion = { version = "4.0.0-rc.1", features = ["place-memory-on-domain"] } # require NUMA-aware allocationsOr for the preview development version:
[dependencies]
forkunion = { git = "https://github.com/ashvardanian/ForkUnion.git", branch = "main-dev" }A minimal example may look like this:
use forkunion as fu;
let topology = fu::Topology::new().expect("Failed to detect hardware topology");
let mut pool = fu::spawn(&topology, 2);
pool.broadcast(|thread_index, compute_domain_index| {
println!("Hello from thread # {} on compute domain # {}", thread_index + 1, compute_domain_index + 1);
});Higher-level APIs distribute index-addressable tasks across the threads in the pool:
pool.for_n(100, |task, at| {
println!("Running task {} on thread # {}", task + 1, at.thread + 1);
})?;
pool.for_slices(100, |range, at| {
println!("Running run [{}, {}) on thread # {}", range.first, range.end(), at.thread + 1);
})?;
pool.for_n_dynamic(100, |task, at| {
println!("Running task {} on thread # {}", task + 1, at.thread + 1);
})?;To let a worker closure borrow the caller’s stack, reach for a scope. Scope dispatches and joins on the calling thread; ScopeView is its Copy read-only half, and is what a worker carries:
let counter = fu::SpinMutex::new(0usize);
pool.scope(|scope| {
let view = scope.view();
scope.broadcast(|thread_index, compute_domain_index| {
let local = view.locate_thread_in(thread_index, compute_domain_index);
println!("thread # {} is # {} within its compute domain", thread_index, local);
*counter.lock() += 1;
});
});Scope is deliberately not Sync, so a worker cannot capture it and start a second dispatch on a pool that is still mid-generation — the pool would refuse it as contended.
Taking a ScopeView first is the one line that separates the two capabilities.
A more realistic example with named threads and error handling may look like this:
use std::error::Error;
use forkunion as fu;
fn heavy_math(_: usize) {}
fn main() -> Result<(), Box<dyn Error>> {
let topology = fu::Topology::new()?;
let mut pool = fu::ThreadPool::named_spawn(&topology, "heavy-math", 4)?;
pool.for_n_dynamic(400, |task, _at| {
heavy_math(task);
})?;
Ok(())
}The Topology handle reports the hardware topology, to size and place work:
let topology = fu::Topology::new()?;
for domain in 0..topology.compute_domains_count()? {
let domain = fu::ComputeDomain(domain);
println!("domain {}: {} cores, level {}, allocate on memory domain {}",
domain.get(), topology.logical_cores_count_in(domain)?,
topology.compute_level_in(domain)?.get(), topology.local_memory_of(domain)?.get());
}For advanced usage, refer to the NUMA section below.
For Rayon-style parallel iterators, import the prelude module and check out related examples.
API
Types
ResultThe result of every fallible call in this crate.
StructThreadPoolMinimalistic, fixed-size thread-pool for blocking scoped parallelism.25
pub struct ThreadPool This is a safe Rust wrapper around the precompiled C thread pool implementation.
The current thread participates in the work, so for N-way parallelism the implementation
actually spawns N − 1 background workers and runs the last slice on the caller thread.
Thread Safety
ThreadPool is Send + Sync and can be safely shared between threads, though operations
require a mutable reference to ensure exclusive access during execution.
Performance Characteristics
- Zero dynamic allocations during task execution
- Leverages weak memory model for optimal ARM and PowerPC performance
- NUMA-aware thread placement when available
- Uses CPU-specific busy-waiting instructions for minimal latency
Generation Tokens
Every dispatch is identified by a usize generation token. The safe for_threads API wraps it
inside a BroadcastJoin guard it lends to the caller: the workers start at construction,
join/Drop waits for them, and on Inclusive pools first runs the calling thread's own
slice, so only Exclusive pools can be polled with is_complete before it. Raw token-level
access is available through the unsafe_for_threads/unsafe_join pair.
Examples
Basic usage with simple computations:
use forkunion::*;
// Create a thread pool with 4 threads
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
// Execute work on each thread
pool.broadcast(|thread_index, compute_domain_index| {
println!("Thread {} on compute_domain {}", thread_index, compute_domain_index);
});
// Distribute 1000 tasks across threads
pool.for_n(1000, |task, _at| {
// Each task gets a unique index via task
let result = task * task;
std::hint::black_box(result); // Prevent optimization
});
See also helper functions like for_each_task_mut for data processing.
Implements
Send, Sync, Dropspawn_with_exclusivitySpawns a pool of the given size with the caller's participation set by exclusivity.
pub fn spawn_with_exclusivity(
topology: &Topology,
threads: usize,
exclusivity: CallerExclusivity,
) -> Result<Self> named_spawn_with_exclusivitySpawns a named pool of the given size with the caller's participation set by exclusivity.
pub fn named_spawn_with_exclusivity(
topology: &Topology,
name: Option<&str>,
threads: usize,
exclusivity: CallerExclusivity,
) -> Result<Self> named_spawn_with_capabilitiesAs named_spawn_with_exclusivity, but constrains the pool to allowed: clear a waiter bit to force a lower-priority busy-wait, or clear Capabilities::PLACE_MEMORY_ON_DOMAIN to force the flat pool, which is not NUMA-aware.
pub fn named_spawn_with_capabilities(
topology: &Topology,
name: Option<&str>,
threads: usize,
exclusivity: CallerExclusivity,
allowed: Capabilities,
) -> Result<Self> spawn_onSpawns a pool pinned to a single compute domain, a same-QoS core cluster.
pub fn spawn_on(
topology: &Topology,
compute_domain_index: usize,
threads: usize,
exclusivity: CallerExclusivity,
) -> Result<Self> The pool's threads and NUMA-local allocations stay on compute_domain_index, in
0..compute_domains_count(). Spawn one such pool per compute domain and coordinate them
from a single thread with the generation-token API (for_threads guards or the raw
unsafe_for_threads/is_complete/unsafe_join). On builds without NUMA, only compute
domain 0 is valid.
Examples
use forkunion::*;
// One pool per compute domain, sized to that domain's core count.
let topology = Topology::new().unwrap();
let pools: Vec<ThreadPool> = (0..topology.compute_domains_count().unwrap())
.map(|c| ThreadPool::spawn_on(&topology, c, topology.logical_cores_count_in(ComputeDomain(c)).unwrap_or(1), CallerExclusivity::Exclusive).unwrap())
.collect();
assert_eq!(pools.len(), topology.compute_domains_count().unwrap());
spawn_on_with_capabilitiesAs spawn_on, but constrains the colocated pool to allowed.
pub fn spawn_on_with_capabilities(
topology: &Topology,
compute_domain_index: usize,
threads: usize,
exclusivity: CallerExclusivity,
allowed: Capabilities,
) -> Result<Self> spawnCreates a new thread pool with the specified number of threads.
pub fn spawn(topology: &Topology, threads: usize) -> Result<Self> By default, uses CallerExclusivity::Inclusive, meaning the calling thread participates in
work execution. For N threads, this uses the caller thread plus N-1 background workers.
Arguments
threads- Total number of threads including the caller thread
Examples
use forkunion::*;
// Create a pool that uses 4 threads total (3 spawned + caller)
let topology = Topology::new().unwrap();
let pool = ThreadPool::spawn(&topology, 4).expect("Failed to create thread pool");
assert_eq!(pool.threads_count(), 4);
named_spawnCreates a new named thread pool with the specified number of threads.
pub fn named_spawn(topology: &Topology, name: &str, threads: usize) -> Result<Self> The thread pool name can be useful for debugging, profiling, and system monitoring. On supported platforms, the name may be visible in system tools and thread listings. Names are truncated to 15 characters, plus a null terminator, to fit platform limits.
Arguments
name- Name for the thread pool, up to 15 charactersthreads- Total number of threads including the caller thread
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let pool = ThreadPool::named_spawn(&topology, "worker_pool", 4).expect("Failed to create thread pool");
assert_eq!(pool.threads_count(), 4);
caller_exclusivityReturns whether the calling thread participates in the workload.
pub fn caller_exclusivity(&self) -> Result<CallerExclusivity> Queries the pool directly rather than caching, so it stays correct across terminate and
re-spawning with a different exclusivity.
capabilitiesReturns the capabilities this pool was spawned with.
pub fn capabilities(&self) -> Result<Capabilities> Queries the pool directly rather than caching, so it reflects the allow-mask the pool actually honors - the intersection of the requested mask and what the machine offers.
compute_domains_countReturns the number of thread compute_domains in the pool.
pub fn compute_domains_count(&self) -> usize Compute domains group threads sharing a memory domain, QoS level, and cache hierarchy. This information is useful for NUMA-aware load balancing and memory allocation.
threads_count_inReturns the number of threads in a specific compute_domain.
pub fn threads_count_in(&self, compute_domain_index: usize) -> usize This method is useful for NUMA-aware load balancing, allowing you to understand how many threads are available in each compute_domain group.
Arguments
compute_domain_index- The compute_domain to query (0-based)
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let pool = spawn(&topology, 8);
let total_compute_domains = pool.compute_domains_count();
for compute_domain_index in 0..total_compute_domains {
let thread_count = pool.threads_count_in(compute_domain_index);
println!("ComputeDomain {} has {} threads", compute_domain_index, thread_count);
}
threads_countReturns the number of threads in the pool.
pub fn threads_count(&self) -> usize locate_thread_inConverts a global thread index to a local thread index within a compute_domain.
pub fn locate_thread_in(&self, global_thread_index: usize, compute_domain_index: usize) -> usize This is useful for distributed thread pools where threads are grouped into compute domains, same-QoS core clusters. The local index can be used for per-compute_domain data structures or algorithms.
Returns
The local thread index within the specified compute_domain.
Arguments
global_thread_index- The global thread index to convertcompute_domain_index- The compute_domain to get the local index for
sleepTransitions worker threads to a power-saving sleep state.
pub fn sleep(&mut self, micros: usize) This function places worker threads into a low-power sleep state when no work is available for extended periods, checking periodically at the specified interval.
Arguments
micros- Wake-up check interval in microseconds, must be > 0
Safety
This function is not thread-safe and should only be called between task batches when no parallel operations are in progress.
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
// Process a batch of work
pool.for_n(1000, |task, _at| {
// Do some work...
std::hint::black_box(task * 2);
});
// Put threads to sleep between batches to save power, until the next dispatch
pool.sleep(0);
// Process another batch
pool.for_n(500, |task, _at| {
std::hint::black_box(task * 3);
});
napParks worker threads on the CPU's address monitor until the next dispatch.
pub fn nap(&mut self) Unlike sleep, the workers stay on their cores and wake with no syscall.
for_threadsExecutes a function on each thread of the pool while caller overlaps its own work.
pub fn for_threads<F, R>(&mut self, function: F, caller: impl FnOnce(&BroadcastJoin<'_>) -> R) -> R
where
F: Fn(usize, usize) + Sync, The workers start right away and caller receives a BroadcastJoin to poll or join. The
join also runs when caller returns or unwinds, on CallerExclusivity::Inclusive pools
first running the calling thread's own slice, so function outlives every worker.
Arguments
function- Closure executed on each thread, receiving(thread_index, compute_domain_index).caller- Closure the calling thread runs with the dispatch underway; its result is returned.
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
pool.for_threads(
|thread_index, compute_domain_index| {
println!("Thread {} on compute_domain {}", thread_index, compute_domain_index);
},
|operation| operation.join(),
);
The guard is only lent, so it cannot be leaked past the dispatch:
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 2);
let escaped = pool.for_threads(|_, _| {}, |operation| operation);
broadcastRuns function on every thread and blocks until all of them finish.
pub fn broadcast<F>(&mut self, function: F)
where
F: Fn(usize, usize) + Sync, The ergonomic common case: unlike for_threads, this takes the closure
by value and joins internally, so there is no & binding or guard to manage. Reach for
for_threads only when you want to overlap the caller's own work with the pool and poll
BroadcastJoin::is_complete before joining.
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
let counter = std::sync::atomic::AtomicUsize::new(0);
pool.broadcast(|_thread_index, _compute_domain_index| {
counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
assert_eq!(counter.load(std::sync::atomic::Ordering::Relaxed), pool.threads_count());
scopeRuns body with a Scope that can broadcast work borrowing local data and answer read-only topology queries, joining every dispatch before returning.
pub fn scope<F, R>(&mut self, body: F) -> R
where
F: FnOnce(&Scope<'_>) -> R, The scope holds the pool by shared reference, so a worker closure can query it and borrow
the same stack values the caller owns - the borrow conflict that otherwise forces a
raw-pointer smuggle. Because each Scope::broadcast blocks until it joins, those borrows
can never outlive the work.
Take a ScopeView before broadcasting to carry the queries into the workers; Scope
itself is not Sync, so a nested dispatch cannot re-enter the pool mid-generation.
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
let counter = SpinMutex::new(0usize);
pool.scope(|scope| {
let view = scope.view();
scope.broadcast(|thread_index, compute_domain_index| {
let _local = view.locate_thread_in(thread_index, compute_domain_index);
*counter.lock() += 1;
});
});
assert_eq!(*counter.lock(), pool.threads_count());
for_slicesDistributes n similar duration calls between threads in slices.
pub fn for_slices<F>(&mut self, n: usize, function: F) -> Result<()>
where
F: Fn(TasksRange, ThreadInDomain) + Sync, Instead of individual task assignment, this method groups tasks into contiguous slices and assigns each slice to a thread. This reduces per-task overhead and improves cache locality.
Arguments
n- Total number of tasks to distributefunction- Closure executed for each run, given itsThreadInDomainand aTasksRange
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
pool.for_slices(1000, |range, at| {
// Each thread gets one contiguous run, which iterates directly
for task in range {
std::hint::black_box(task * task);
}
println!("Thread {} processed run [{}, {})", at.thread, range.first, range.end());
});
for_slices_mutSplits data into one contiguous chunk per thread and runs function on each in parallel, blocking until all threads finish.
pub fn for_slices_mut<T, F>(&mut self, data: &mut [T], function: F) -> Result<()>
where
T: Send,
F: Fn(&mut [T], ThreadInDomain) + Sync, Each thread receives an exclusive &mut sub-slice, so no interior mutability, Mutex,
or raw pointers are needed at the call site: the chunks partition data and therefore never
alias, and the synchronous join keeps every borrow inside data's lifetime. This is the
safe replacement for a hand-rolled raw-pointer scatter.
Built on for_threads, so function runs once per thread even when
its chunk is empty - unlike for_slices, which skips an empty range.
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
let mut data = vec![0u64; 1000];
pool.for_slices_mut(&mut data, |chunk, _at| {
for value in chunk {
*value += 1;
}
});
assert!(data.iter().all(|&value| value == 1));
for_nDistributes n similar duration calls between threads by individual indices.
pub fn for_n<F>(&mut self, n: usize, function: F) -> Result<()>
where
F: Fn(usize, ThreadInDomain) + Sync, Uses static load balancing where each thread gets a predetermined set of tasks. This is optimal when all tasks have similar execution time.
Arguments
n- Total number of tasks to distributefunction- Closure executed for each task, given the task index and itsThreadInDomain
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
pool.for_n(1000, |task, _at| {
// Simulate computation based on task index
let result = task * task;
std::hint::black_box(result); // Prevent optimization
});
for_n_dynamicExecutes n uneven tasks on all threads, greedily stealing work.
pub fn for_n_dynamic<F>(&mut self, n: usize, function: F) -> Result<()>
where
F: Fn(usize, ThreadInDomain) + Sync, Uses dynamic load balancing with work-stealing. Threads that finish their assigned tasks early will steal work from busy threads. This is optimal when task execution times vary significantly.
Arguments
n- Total number of tasks to distributefunction- Closure executed for each task, given the task index and itsThreadInDomain
Examples
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 4);
pool.for_n_dynamic(100, |task, _at| {
// Simulate variable work duration - some tasks take longer
let iterations = if task % 10 == 0 { 10000 } else { 100 };
for i in 0..iterations {
std::hint::black_box(task * i);
}
});
unsafe_for_threadsDispatches callback on every thread without blocking, returning the generation token.
pub unsafe fn unsafe_for_threads(
&self,
callback: extern "C" fn(*mut c_void, usize, usize),
context: *mut c_void,
) -> Result<usize> This is the raw C-ABI mirror for building custom orchestration; prefer the safe
BroadcastJoin guard for_threads lends.
Safety
- Only one thread may operate the pool at a time, and only one dispatch may be in
flight:
unsafe_joinmust complete before the next dispatch or pool destruction. callbackandcontextmust remain valid untilunsafe_joinreturns.
is_completeReturns true if the given generation has completed on all threads.
pub fn is_complete(&self, generation: usize) -> Result<bool> A true result also guarantees visibility of every contributor's writes. On Inclusive
pools this can only turn true once unsafe_join contributes the calling thread's slice,
so the poll-then-join pattern is reserved for Exclusive pools.
unsafe_joinBlocks until the given generation completes; idempotent for joined generations.
pub unsafe fn unsafe_join(&self, generation: usize) On Inclusive pools this also executes the calling thread's slice of the work.
Safety
Must be called on the thread operating the pool; the dispatched callback and context must still be valid.
StructBroadcastJoinA synchronization guard that waits for all threads to finish the broadcasted closure.3
pub struct BroadcastJoin<'pool> The closure is dispatched at construction, so the workers start immediately and the caller
can overlap its own work. join, or Drop, waits for completion, and on
CallerExclusivity::Inclusive pools first runs the calling thread's own slice, so only
CallerExclusivity::Exclusive pools may poll is_complete before it.
Only ever lent to the closure given to ThreadPool::for_threads, so it cannot be leaked past
the dispatch: a mem::forget of an owned guard would let workers outlive what they call.
Implements
DropgenerationThe generation token of this broadcast.
pub fn generation(&self) -> usize is_completeTrue once the generation has fully completed on all threads.
pub fn is_complete(&self) -> bool joinWaits for every thread, running the caller's own slice first on Inclusive pools.
pub fn join(&self) Idempotent: the core returns at once for a generation already joined.
StructScopeA borrow-scoped handle to a thread pool, yielded by ThreadPool::scope.6
pub struct Scope<'pool> Holding the pool by shared reference is what lets a worker closure borrow the caller's stack
data - the borrow conflict that otherwise forces a raw-pointer smuggle. Every
Scope::broadcast joins before returning, so those borrows are always valid.
Not [Sync]: the queries travel into workers as a ScopeView, while dispatch stays on the
calling thread. A worker that captured the scope could re-enter the pool mid-generation:
use forkunion::*;
let topology = Topology::new().unwrap();
let mut pool = spawn(&topology, 2);
pool.scope(|scope| {
scope.broadcast(|_thread_index, _compute_domain_index| {
scope.broadcast(|_, _| {});
});
});
viewThe query-only half, which a worker closure may capture.
pub fn view(&self) -> ScopeView<'pool> threads_*_count× 2Total number of worker threads in the pool.
pub fn threads__count(&self) -> usize compute_domains_*_domains_count× 2Number of compute domains the pool spans.
pub fn compute_domains__domains_count(&self) -> usize threads_count_*_count_in× 2Number of threads pinned to the given compute domain.
pub fn threads_count__count_in(&self, compute_domain_index: usize) -> usize locate_thread_*_thread_in× 2Local index of a global thread within its compute domain.
pub fn locate_thread__thread_in(&self, global_thread_index: usize, compute_domain_index: usize) -> usize broadcastBroadcasts function to every thread and blocks until all of them finish.
pub fn broadcast<F>(&self, function: F)
where
F: Fn(usize, usize) + Sync, The closure is borrowed for the dispatch and joined before this returns, so it may freely
borrow the stack data enclosing the ThreadPool::scope call.
StructScopeViewThe read-only half of a Scope, Copy and Sync, so worker closures can carry it.
pub struct ScopeView<'pool> Scope itself is deliberately not Sync: dispatching from inside a dispatch re-enters the
pool mid-generation. Take a view with Scope::view before broadcasting, and query through it.
Implements
Clone, CopyTraitForDispatchHow a staged dispatch hands itself to the pool when its guard drops.1
pub trait ForDispatch<F> Each marker fixes one C entry point and, through the bound on its impl, the callback arity that entry point calls back with.
Implementors
impl<F> ForDispatch<F> for StaticIndices where F: Fn(usize, ThreadInDomain) + Syncimpl<F> ForDispatch<F> for DynamicIndices where F: Fn(usize, ThreadInDomain) + Syncimpl<F> ForDispatch<F> for StaticSlices where F: Fn(TasksRange, ThreadInDomain) + Sync
*× 4Runs tasks_count tasks through function, blocking until every thread finishes.
fn dispatch(pool: &mut ThreadPool, tasks_count: usize, function: &F) -> Result<()> StructStaticIndicesOne call per index, statically partitioned across threads.
StructDynamicIndicesOne call per index, greedily stolen by whichever thread frees up first.
StructStaticSlicesOne call per contiguous run of indices, statically partitioned across threads.
StructCapabilitiesEverything the library can do, whether decided when it was compiled or found on this machine.30
pub struct Capabilities(pub u32); One bit per facility, and two accessors ask two questions of the same bit.
comptime_capabilities reports whether the code was built: a set
Capabilities::PLACE_HUGE_PAGES_ON_DOMAIN means we compiled the path that asks for them.
runtime_capabilities reports whether the machine offers it now.
Neither implies the other. A binary that built Capabilities::PLACE_MEMORY_ON_DOMAIN runs
perfectly well on a single-node box, where the runtime accessor never sets that bit; and a
machine with four NUMA nodes reports none of them to a build that left the topology out.
use forkunion::{comptime_capabilities, Capabilities};
if comptime_capabilities().contains(Capabilities::COLOCATE_POOLS_ON_DOMAIN) {
// `spawn_in`, `DomainAllocator`, and friends are real here.
}
Fields
0 | u32 |
|---|
Implements
Clone, Copy, StructuralPartialEq, PartialEq, Eq, Hash, DebugNONENothing detected, or nothing compiled in.
pub const NONE: Capabilities = _; X86_PAUSEx86 pause instruction.
pub const X86_PAUSE: Capabilities = _; X86_TPAUSEx86-64 tpause instruction, with WAITPKG support.
pub const X86_TPAUSE: Capabilities = _; ARM64_YIELDArm yield instruction.
pub const ARM64_YIELD: Capabilities = _; ARM64_WFETAArch64 wfet instruction, with FEAT_WFxT support.
pub const ARM64_WFET: Capabilities = _; RISCV64_PAUSERISC-V pause instruction.
pub const RISCV64_PAUSE: Capabilities = _; RISCV64_WRSRISC-V WRS.STO monitored wait, from the Zawrs extension.
pub const RISCV64_WRS: Capabilities = _; OS_THREADSOwn the raw OS thread handle instead of a std::thread.
pub const OS_THREADS: Capabilities = _; TOPOLOGYEnumerate this machine's cores, compute domains, and memory domains.
pub const TOPOLOGY: Capabilities = _; PLACE_THREADS_BY_AFFINITYBind a thread to a set of cores, choosing where it runs.
pub const PLACE_THREADS_BY_AFFINITY: Capabilities = _; PLACE_THREADS_BY_CORE_CLASSSteer a thread onto a class of core at creation, choosing where it runs.
pub const PLACE_THREADS_BY_CORE_CLASS: Capabilities = _; RESCHEDULE_THREADS_BY_CLASSReclass a thread's scheduler to sleep or wake it, choosing when it runs.
pub const RESCHEDULE_THREADS_BY_CLASS: Capabilities = _; PLACE_MEMORY_ON_DOMAINPlace a buffer's pages on a chosen memory domain.
pub const PLACE_MEMORY_ON_DOMAIN: Capabilities = _; PLACE_HUGE_PAGES_ON_DOMAINPlace larger-than-base pages on a chosen memory domain.
pub const PLACE_HUGE_PAGES_ON_DOMAIN: Capabilities = _; HUGE_TRANSPARENT_PAGESThe kernel promotes base pages to huge pages on its own.
pub const HUGE_TRANSPARENT_PAGES: Capabilities = _; COLOCATE_POOLS_ON_DOMAINThe domain-aware colocated_pool and distributed_pool are compiled in.
pub const COLOCATE_POOLS_ON_DOMAIN: Capabilities = _; X86_CLDEMOTECLDEMOTE moves a just-written line toward the shared LLC and retains it.
pub const X86_CLDEMOTE: Capabilities = _; Reporting-only:
the emitter is chosen at compile time by FORKUNION_WITH_DEMOTE_CACHE_LINES, never
dispatched.
ARM64_DC_CVACDC CVAC cleans a dirty line to the coherency point - AArch64's nearest demote.
pub const ARM64_DC_CVAC: Capabilities = _; Set where
EL0 execution is known-legal, i.e. Linux, which sets SCTLR_EL1.UCI.
RISCV64_ZICBOMThe kernel enabled user-mode Zicbom cache-block management, attested through hwprobe - the bit the C ABI's cascade selects cbo.clean on.
pub const RISCV64_ZICBOM: Capabilities = _; X86_CMPCCXADDCMPCCXADD, the conditional atomic add, admitting x86_cmpccxadd_atomic_ref.
pub const X86_CMPCCXADD: Capabilities = _; X86_RAOINTRAO-INT aadd and its family, executed at the shared cache.
pub const X86_RAOINT: Capabilities = _; ARM64_LSEArmv8.1 FEAT_LSE: one instruction per read-modify-write.
pub const ARM64_LSE: Capabilities = _; ARM64_RCPCArmv8.3 FEAT_LRCPC: LDAPR acquires, cheaper than LDAR's.
pub const ARM64_RCPC: Capabilities = _; RISCV64_ZACASRISC-V Zacas, the amocas compare-and-swap.
pub const RISCV64_ZACAS: Capabilities = _; RISCV64_ATOMICThe RISC-V A extension's lr, sc and amo words.
pub const RISCV64_ATOMIC: Capabilities = _; ARM64_PRFMPRFM, AArch64's prefetch hint.
pub const ARM64_PRFM: Capabilities = _; RISCV64_ZICBOPRISC-V Zicbop, the prefetch.w hint.
pub const RISCV64_ZICBOP: Capabilities = _; RISCV64_ZABHARISC-V Zabha, byte and halfword atomics.
pub const RISCV64_ZABHA: Capabilities = _; ALLAll-ones allow-mask: pass to a pool constructor to disable capability filtering.
pub const ALL: Capabilities = _; containsWhether every bit of other is set in self.
pub const fn contains(self, other: Capabilities) -> bool EnumCallerExclusivityDefines whether the calling thread participates in task execution.
pub enum CallerExclusivity Values
Inclusive | 0 | The calling thread participates in the workload, spawning N-1 workers |
|---|---|---|
Exclusive | 1 | The calling thread only coordinates, doesn't execute tasks, spawning N workers |
Implements
Copy, Clone, Debug, StructuralPartialEq, PartialEq, EqEnumStatusWhy a call into the C core failed, in the vocabulary the core itself uses.1
pub enum Status Mirrors fu_status_t value-for-value, so the FFI boundary only retypes. Unrecognized covers a
status a newer core reports and this build has no name for.
Values
Unknown | -1 | No reason was reported, or one this build does not name. |
|---|---|---|
BadAlloc | -2 | An allocation or mapping failed; a smaller request may succeed. |
CapacityExhausted | -3 | A fixed ceiling was reached, so a smaller request will not help either. |
InvalidArgument | -4 | An argument was malformed, out of range, or would overflow a byte count. |
ConfigMismatch | -5 | The handles or the pool kind cannot serve this call together. |
AlreadySpawned | -6 | The pool is already spawned; terminate it first. |
NotSpawned | -7 | The pool was never spawned. |
ThreadRefused | -8 | The OS declined to create a thread - a resource limit, or permissions. |
TopologyUnavailable | -9 | The machine could not be described; transient if its CPU set changed mid-probe. |
PermissionDenied | -10 | A privileged operation was declined. |
Unsupported | -11 | This build or this machine has no such facility. |
Contended | -14 | The pool is running another dispatch: another caller's, or the one this call runs inside. |
Unrecognized | i32::MIN | A status this binding has no name for. |
Implements
Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, HashdescribeStatic, English description; mirrors fu_status_to_string.
pub fn describe(self) -> &'static str StructErrorA failure, whether the core reported it or the binding caught it before the call.
pub struct Error detail names the symbol or the argument, which is what turns a status into a diagnosis.
Both fields are plain data, so nothing allocates on the failure path - which is what lets this
crate stay no_std.
Fields
status | Status | The identity of the failure, shared with the C interface. |
|---|---|---|
detail | &'static str | The detail the status alone cannot carry. |
Implements
StructTasksRangeA half-open [first, first + count) run of task indices - the "what work" of a slice dispatch.3
pub struct TasksRange Iterable, so a callback reads for task in range rather than rebuilding the bounds. An idle
thread receives a range with count == 0, which every dispatch still calls exactly once.
Fields
first | usize | The first task index in the run. |
|---|---|---|
count | usize | How many tasks the run covers; zero means an idle thread. |
Implements
Copy, Clone, Debug, StructuralPartialEq, PartialEq, Eq, IntoIteratorendOne past the last task index, so first..end() is the half-open span.
pub fn end(&self) -> usize is_emptyWhether the run covers no tasks at all.
pub fn is_empty(&self) -> bool rangeThe run as a Range, for indexing a slice or driving a for loop.
pub fn range(&self) -> Range<usize> StructSyncConstPtrA thread-safe wrapper around raw pointers for sharing read-only data across threads.3
pub struct SyncConstPtr<T> This type is designed for scenarios where you need to share immutable data across async tasks or threads, particularly when the standard borrowing rules would prevent such sharing. The caller is responsible for ensuring that:
- The pointed-to data remains valid for the lifetime of use
- The data is not modified while being accessed through
SyncConstPtr
Safety
This type is marked as Send + Sync but requires careful usage:
- Only use with data that won't be modified during the lifetime of the pointer
- Ensure the pointed-to data outlives all uses of the
SyncConstPtr - The
getmethod is unsafe and requires the caller to ensure bounds checking
Examples
use forkunion::*;
let data = vec![1, 2, 3, 4, 5];
let sync_ptr = SyncConstPtr::new(data.as_ptr());
// Safe to use in async contexts
let value = unsafe { sync_ptr.get(0) };
assert_eq!(*value, 1);
Implements
Clone, Copy, Debug, Send, SyncnewWraps a raw pointer; every obligation is discharged at get.
pub fn new(ptr: *const T) -> Self getGets a reference to the element at the given index.
pub unsafe fn get(&self, index: usize) -> &T Returns
A reference to the element at the given index.
Safety
The caller must ensure that:
- The index is within bounds of the allocated data
- The data at the index is properly initialized
- The data remains valid for the lifetime of the returned reference
Arguments
index- The index of the element to access
as_*_ptr× 2Returns the raw pointer.
pub fn as__ptr(&self) -> *const T StructSyncMutPtrA mutable raw pointer that is Send and Sync, for disjoint writes from many threads.2
newWraps a raw pointer; every obligation is discharged at get.
pub const fn new(ptr: *mut T) -> Self getReturns a mutable pointer to the element at the given index.
pub unsafe fn get(&self, index: usize) -> *mut T Safety
The caller must ensure:
- The index is within the bounds of the original allocation
- No overlapping mutable access occurs from multiple threads
- Each thread accesses disjoint indices when used concurrently
- The pointer remains valid for the duration of access
StructIndexedSplitSplits a range of tasks into fair-sized runs for parallel distribution.2
pub struct IndexedSplit The first (tasks % threads) runs have size ceil(tasks / threads).
The remaining runs have size floor(tasks / threads).
This ensures optimal load balancing across threads with minimal size variance. See: https://lemire.me/blog/2025/05/22/dividing-an-array-into-fair-sized-chunks/
Implements
Debug, ClonenewCreates a new indexed split for distributing tasks across threads.
pub fn new(tasks_count: usize, threads_count: usize) -> Self Arguments
tasks_count- Total number of tasks to distributethreads_count- Number of threads to distribute across (must be > 0)
Panics
Panics if threads_count is zero.
getReturns the range for a specific thread index.
pub fn get(&self, thread_index: usize) -> Range<usize> Functions
spawnSpawns a pool with the specified number of threads.
pub fn spawn(topology: &Topology, threads: usize) -> ThreadPool named_spawnSpawns a named pool with the specified number of threads.
pub fn named_spawn(topology: &Topology, name: &str, threads: usize) -> ThreadPool for_nStandalone function to distribute n similar duration calls between threads.
pub fn for_n<F>(pool: &mut ThreadPool, n: usize, function: F) -> Result<()>
where
F: Fn(usize, ThreadInDomain) + Sync, for_n_dynamicStandalone function to execute n uneven tasks on all threads.
pub fn for_n_dynamic<F>(pool: &mut ThreadPool, n: usize, function: F) -> Result<()>
where
F: Fn(usize, ThreadInDomain) + Sync, for_slicesStandalone function to distribute n tasks in slices.
pub fn for_slices<F>(pool: &mut ThreadPool, n: usize, function: F) -> Result<()>
where
F: Fn(TasksRange, ThreadInDomain) + Sync, for_each_task_mutHelper function to visit every element exactly once with mutable access.
pub fn for_each_task_mut<T, F>(pool: &mut ThreadPool, data: &mut [T], function: F) -> Result<()>
where
T: Send,
F: Fn(&mut T, usize, ThreadInDomain) + Sync, for_each_task_mut_dynamicHelper function to visit every element exactly once with dynamic work-stealing.
pub fn for_each_task_mut_dynamic<T, F>(
pool: &mut ThreadPool,
data: &mut [T],
function: F,
) -> Result<()>
where
T: Send,
F: Fn(&mut T, usize, ThreadInDomain) + Sync, version_*× 3Returns the major version number of the ForkUnion library.
versionReturns the library version as a tuple of (major, minor, patch).
pub fn version() -> (usize, usize, usize) comptime_capabilitiesWhich kernel facilities this build of ForkUnion was compiled to use.
pub fn comptime_capabilities() -> Capabilities name_capabilitiesFormats a capability bitset into a comma-separated name list, like "threads,topology".
pub fn name_capabilities(caps: Capabilities) -> Result<String> POLISH: returns an owned String, since fu_name_capabilities writes into a caller buffer, not
a static pointer; a fixed-capacity stack type would avoid this allocation.
comptime_capabilities_stringThe set comptime_capabilities bits, comma-separated, like "threads,topology".
pub fn comptime_capabilities_string() -> Result<String> runtime_capabilitiesWhich features this machine turned out to offer, probing the CPU and the memory system.
pub fn runtime_capabilities() -> Capabilities runtime_capabilities_stringThe set runtime_capabilities bits, comma-separated, like "arm64_yield,numa_aware".
pub fn runtime_capabilities_string() -> Result<String> bytes_for_elementsBytes occupied by count elements of element_bytes each, refusing a product that would wrap.
pub fn bytes_for_elements(count: usize, element_bytes: usize) -> Result<usize> Constants
DEFAULT_ALIGNMENTDefault alignment for preventing false sharing between threads.
pub const DEFAULT_ALIGNMENT: usize = 128usize; Picked from the target the way FORKUNION_DEFAULT_ALIGNMENT is in types.hpp: 256 bytes on
s390x, 64 on wasm, 128 elsewhere.
On x86, most CPUs fetch 2 cache lines (128 bytes) at once with spatial prefetching enabled. This conservative padding prevents false sharing even with aggressive prefetch settings. Only padding rests on this - a thread pool strides its cells by the width the machine itself reports at spawn, which is exact where this is a guess.