Rust · Quickstart

Quickstart

use forkunion::ThreadPool;use forkunion::BroadcastJoin;use forkunion::Scope;use forkunion::ScopeView;use forkunion::ForDispatch;use forkunion::StaticIndices;use forkunion::DynamicIndices;use forkunion::StaticSlices;use forkunion::Capabilities;use forkunion::CallerExclusivity;use forkunion::Status;use forkunion::Error;use forkunion::TasksRange;use forkunion::SyncConstPtr;use forkunion::SyncMutPtr;use forkunion::IndexedSplit;use forkunion::*; 4.0.0-rc.1 · d927445f

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:

  • spawn to initialize worker threads, and
  • for_threads to 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_t is backed by linux_numa_allocator_t, freebsd_numa_allocator_t, or windows_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 allocations

Or 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.
types.rs:126 ↗
pub type Result<T> = Result<T, Error>;
StructThreadPoolMinimalistic, fixed-size thread-pool for blocking scoped parallelism.25
scheduling.rs:153 ↗
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, Drop
spawn_with_exclusivitySpawns a pool of the given size with the caller's participation set by exclusivity.
scheduling.rs:162 ↗
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.
scheduling.rs:167 ↗
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.
scheduling.rs:179 ↗
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.
scheduling.rs:237 ↗
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.
scheduling.rs:247 ↗
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.
scheduling.rs:294 ↗
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.
scheduling.rs:318 ↗
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 characters
  • threads - 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.
scheduling.rs:326 ↗
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.
scheduling.rs:343 ↗
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.
scheduling.rs:357 ↗
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.
scheduling.rs:391 ↗
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.
scheduling.rs:403 ↗
pub fn threads_count(&self) -> usize
locate_thread_inConverts a global thread index to a local thread index within a compute_domain.
scheduling.rs:427 ↗
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 convert
  • compute_domain_index - The compute_domain to get the local index for
sleepTransitions worker threads to a power-saving sleep state.
scheduling.rs:474 ↗
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.
scheduling.rs:483 ↗
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.
scheduling.rs:525 ↗
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.
scheduling.rs:553 ↗
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.
scheduling.rs:589 ↗
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.
scheduling.rs:627 ↗
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 distribute
  • function - Closure executed for each run, given its ThreadInDomain and a TasksRange

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.
scheduling.rs:659 ↗
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.
scheduling.rs:699 ↗
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 distribute
  • function - Closure executed for each task, given the task index and its ThreadInDomain

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.
scheduling.rs:733 ↗
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 distribute
  • function - Closure executed for each task, given the task index and its ThreadInDomain

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.
scheduling.rs:750 ↗
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_join must complete before the next dispatch or pool destruction.
  • callback and context must remain valid until unsafe_join returns.
is_completeReturns true if the given generation has completed on all threads.
scheduling.rs:768 ↗
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.
scheduling.rs:785 ↗
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
scheduling.rs:926 ↗
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

Drop
generationThe generation token of this broadcast.
scheduling.rs:956 ↗
pub fn generation(&self) -> usize
is_completeTrue once the generation has fully completed on all threads.
scheduling.rs:966 ↗
pub fn is_complete(&self) -> bool

A true result also guarantees visibility of every contributor's writes. On Inclusive pools this can only turn true once join contributes the calling thread's slice, so the poll-then-join pattern is reserved for Exclusive pools.

joinWaits for every thread, running the caller's own slice first on Inclusive pools.
scheduling.rs:974 ↗
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
scheduling.rs:1004 ↗
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.
scheduling.rs:1049 ↗
pub fn view(&self) -> ScopeView<'pool>
threads_*_count× 2Total number of worker threads in the pool.
scheduling.rs:1055 ↗
pub fn threads__count(&self) -> usize
compute_domains_*_domains_count× 2Number of compute domains the pool spans.
scheduling.rs:1061 ↗
pub fn compute_domains__domains_count(&self) -> usize
threads_count_*_count_in× 2Number of threads pinned to the given compute domain.
scheduling.rs:1067 ↗
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.
scheduling.rs:1073 ↗
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.
scheduling.rs:1081 ↗
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.
scheduling.rs:1016 ↗
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, Copy
TraitForDispatchHow a staged dispatch hands itself to the pool when its guard drops.1
scheduling.rs:1108 ↗
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

*× 4Runs tasks_count tasks through function, blocking until every thread finishes.
scheduling.rs:1110 ↗
fn dispatch(pool: &mut ThreadPool, tasks_count: usize, function: &F) -> Result<()>
StructStaticIndicesOne call per index, statically partitioned across threads.
scheduling.rs:1115 ↗
pub struct StaticIndices;

Implements

Clone, Copy, Debug, ForDispatch<F>
StructDynamicIndicesOne call per index, greedily stolen by whichever thread frees up first.
scheduling.rs:1119 ↗
pub struct DynamicIndices;

Implements

Clone, Copy, Debug, ForDispatch<F>
StructStaticSlicesOne call per contiguous run of indices, statically partitioned across threads.
scheduling.rs:1123 ↗
pub struct StaticSlices;

Implements

Clone, Copy, Debug, ForDispatch<F>
StructCapabilitiesEverything the library can do, whether decided when it was compiled or found on this machine.30
topology.rs:67 ↗
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

0u32

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Hash, Debug
NONENothing detected, or nothing compiled in.
topology.rs:71 ↗
pub const NONE: Capabilities = _;
X86_PAUSEx86 pause instruction.
topology.rs:74 ↗
pub const X86_PAUSE: Capabilities = _;
X86_TPAUSEx86-64 tpause instruction, with WAITPKG support.
topology.rs:76 ↗
pub const X86_TPAUSE: Capabilities = _;
ARM64_YIELDArm yield instruction.
topology.rs:78 ↗
pub const ARM64_YIELD: Capabilities = _;
ARM64_WFETAArch64 wfet instruction, with FEAT_WFxT support.
topology.rs:80 ↗
pub const ARM64_WFET: Capabilities = _;
RISCV64_PAUSERISC-V pause instruction.
topology.rs:82 ↗
pub const RISCV64_PAUSE: Capabilities = _;
RISCV64_WRSRISC-V WRS.STO monitored wait, from the Zawrs extension.
topology.rs:84 ↗
pub const RISCV64_WRS: Capabilities = _;
OS_THREADSOwn the raw OS thread handle instead of a std::thread.
topology.rs:87 ↗
pub const OS_THREADS: Capabilities = _;
TOPOLOGYEnumerate this machine's cores, compute domains, and memory domains.
topology.rs:89 ↗
pub const TOPOLOGY: Capabilities = _;
PLACE_THREADS_BY_AFFINITYBind a thread to a set of cores, choosing where it runs.
topology.rs:91 ↗
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.
topology.rs:93 ↗
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.
topology.rs:95 ↗
pub const RESCHEDULE_THREADS_BY_CLASS: Capabilities = _;
PLACE_MEMORY_ON_DOMAINPlace a buffer's pages on a chosen memory domain.
topology.rs:97 ↗
pub const PLACE_MEMORY_ON_DOMAIN: Capabilities = _;
PLACE_HUGE_PAGES_ON_DOMAINPlace larger-than-base pages on a chosen memory domain.
topology.rs:99 ↗
pub const PLACE_HUGE_PAGES_ON_DOMAIN: Capabilities = _;
HUGE_TRANSPARENT_PAGESThe kernel promotes base pages to huge pages on its own.
topology.rs:101 ↗
pub const HUGE_TRANSPARENT_PAGES: Capabilities = _;
COLOCATE_POOLS_ON_DOMAINThe domain-aware colocated_pool and distributed_pool are compiled in.
topology.rs:103 ↗
pub const COLOCATE_POOLS_ON_DOMAIN: Capabilities = _;
X86_CLDEMOTECLDEMOTE moves a just-written line toward the shared LLC and retains it.
topology.rs:108 ↗
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.
topology.rs:111 ↗
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.
topology.rs:114 ↗
pub const RISCV64_ZICBOM: Capabilities = _;
X86_CMPCCXADDCMPCCXADD, the conditional atomic add, admitting x86_cmpccxadd_atomic_ref.
topology.rs:116 ↗
pub const X86_CMPCCXADD: Capabilities = _;
X86_RAOINTRAO-INT aadd and its family, executed at the shared cache.
topology.rs:118 ↗
pub const X86_RAOINT: Capabilities = _;
ARM64_LSEArmv8.1 FEAT_LSE: one instruction per read-modify-write.
topology.rs:120 ↗
pub const ARM64_LSE: Capabilities = _;
ARM64_RCPCArmv8.3 FEAT_LRCPC: LDAPR acquires, cheaper than LDAR's.
topology.rs:122 ↗
pub const ARM64_RCPC: Capabilities = _;
RISCV64_ZACASRISC-V Zacas, the amocas compare-and-swap.
topology.rs:124 ↗
pub const RISCV64_ZACAS: Capabilities = _;
RISCV64_ATOMICThe RISC-V A extension's lr, sc and amo words.
topology.rs:126 ↗
pub const RISCV64_ATOMIC: Capabilities = _;
ARM64_PRFMPRFM, AArch64's prefetch hint.
topology.rs:128 ↗
pub const ARM64_PRFM: Capabilities = _;
RISCV64_ZICBOPRISC-V Zicbop, the prefetch.w hint.
topology.rs:130 ↗
pub const RISCV64_ZICBOP: Capabilities = _;
RISCV64_ZABHARISC-V Zabha, byte and halfword atomics.
topology.rs:132 ↗
pub const RISCV64_ZABHA: Capabilities = _;
ALLAll-ones allow-mask: pass to a pool constructor to disable capability filtering.
topology.rs:135 ↗
pub const ALL: Capabilities = _;
containsWhether every bit of other is set in self.
topology.rs:139 ↗
pub const fn contains(self, other: Capabilities) -> bool
EnumCallerExclusivityDefines whether the calling thread participates in task execution.
topology.rs:540 ↗
pub enum CallerExclusivity

Values

Inclusive0The calling thread participates in the workload, spawning N-1 workers
Exclusive1The calling thread only coordinates, doesn't execute tasks, spawning N workers

Implements

Copy, Clone, Debug, StructuralPartialEq, PartialEq, Eq
EnumStatusWhy a call into the C core failed, in the vocabulary the core itself uses.1
types.rs:21 ↗
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-1No reason was reported, or one this build does not name.
BadAlloc-2An allocation or mapping failed; a smaller request may succeed.
CapacityExhausted-3A fixed ceiling was reached, so a smaller request will not help either.
InvalidArgument-4An argument was malformed, out of range, or would overflow a byte count.
ConfigMismatch-5The handles or the pool kind cannot serve this call together.
AlreadySpawned-6The pool is already spawned; terminate it first.
NotSpawned-7The pool was never spawned.
ThreadRefused-8The OS declined to create a thread - a resource limit, or permissions.
TopologyUnavailable-9The machine could not be described; transient if its CPU set changed mid-probe.
PermissionDenied-10A privileged operation was declined.
Unsupported-11This build or this machine has no such facility.
Contended-14The pool is running another dispatch: another caller's, or the one this call runs inside.
Unrecognizedi32::MINA status this binding has no name for.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, Hash
describeStatic, English description; mirrors fu_status_to_string.
types.rs:71 ↗
pub fn describe(self) -> &'static str
StructErrorA failure, whether the core reported it or the binding caught it before the call.
types.rs:97 ↗
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

statusStatusThe identity of the failure, shared with the C interface.
detail&'static strThe detail the status alone cannot carry.

Implements

Clone, Copy, StructuralPartialEq, PartialEq, Eq, Debug, Display, Error
StructTasksRangeA half-open [first, first + count) run of task indices - the "what work" of a slice dispatch.3
types.rs:472 ↗
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

firstusizeThe first task index in the run.
countusizeHow many tasks the run covers; zero means an idle thread.

Implements

Copy, Clone, Debug, StructuralPartialEq, PartialEq, Eq, IntoIterator
endOne past the last task index, so first..end() is the half-open span.
types.rs:482 ↗
pub fn end(&self) -> usize
is_emptyWhether the run covers no tasks at all.
types.rs:488 ↗
pub fn is_empty(&self) -> bool
rangeThe run as a Range, for indexing a slice or driving a for loop.
types.rs:494 ↗
pub fn range(&self) -> Range<usize>
StructSyncConstPtrA thread-safe wrapper around raw pointers for sharing read-only data across threads.3
types.rs:549 ↗
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 get method 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, Sync
newWraps a raw pointer; every obligation is discharged at get.
types.rs:556 ↗
pub fn new(ptr: *const T) -> Self
getGets a reference to the element at the given index.
types.rs:578 ↗
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.
types.rs:585 ↗
pub fn as__ptr(&self) -> *const T
StructSyncMutPtrA mutable raw pointer that is Send and Sync, for disjoint writes from many threads.2
types.rs:595 ↗
pub struct SyncMutPtr<T>

Implements

Clone, Copy, Send, Sync
newWraps a raw pointer; every obligation is discharged at get.
types.rs:603 ↗
pub const fn new(ptr: *mut T) -> Self
getReturns a mutable pointer to the element at the given index.
types.rs:621 ↗
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
types.rs:644 ↗
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, Clone
newCreates a new indexed split for distributing tasks across threads.
types.rs:661 ↗
pub fn new(tasks_count: usize, threads_count: usize) -> Self

Arguments

  • tasks_count - Total number of tasks to distribute
  • threads_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.
types.rs:672 ↗
pub fn get(&self, thread_index: usize) -> Range<usize>

Functions

spawnSpawns a pool with the specified number of threads.
scheduling.rs:1242 ↗
pub fn spawn(topology: &Topology, threads: usize) -> ThreadPool
named_spawnSpawns a named pool with the specified number of threads.
scheduling.rs:1248 ↗
pub fn named_spawn(topology: &Topology, name: &str, threads: usize) -> ThreadPool
for_nStandalone function to distribute n similar duration calls between threads.
scheduling.rs:1253 ↗
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.
scheduling.rs:1261 ↗
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.
scheduling.rs:1269 ↗
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.
scheduling.rs:1277 ↗
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.
scheduling.rs:1292 ↗
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.
topology.rs:146 ↗
pub fn version_major() -> usize
Symbol
version_major
version_minor
version_patch
versionReturns the library version as a tuple of (major, minor, patch).
topology.rs:164 ↗
pub fn version() -> (usize, usize, usize)
comptime_capabilitiesWhich kernel facilities this build of ForkUnion was compiled to use.
topology.rs:170 ↗
pub fn comptime_capabilities() -> Capabilities
name_capabilitiesFormats a capability bitset into a comma-separated name list, like "threads,topology".
topology.rs:179 ↗
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".
topology.rs:202 ↗
pub fn comptime_capabilities_string() -> Result<String>
runtime_capabilitiesWhich features this machine turned out to offer, probing the CPU and the memory system.
topology.rs:208 ↗
pub fn runtime_capabilities() -> Capabilities
runtime_capabilities_stringThe set runtime_capabilities bits, comma-separated, like "arm64_yield,numa_aware".
topology.rs:214 ↗
pub fn runtime_capabilities_string() -> Result<String>
bytes_for_elementsBytes occupied by count elements of element_bytes each, refusing a product that would wrap.
types.rs:138 ↗
pub fn bytes_for_elements(count: usize, element_bytes: usize) -> Result<usize>

Constants

DEFAULT_ALIGNMENTDefault alignment for preventing false sharing between threads.
types.rs:157 ↗
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.