C++ · Parallel Iterators

Parallel Iterators

4.0.0-rc.1 · d927445f

For Rayon-style ergonomics, use the parallel iterator API with the prelude. Unlike Rayon, ForkUnion’s parallel iterators don’t depend on the global state and allow explicit control over the thread pool and scheduling strategy. For statically shaped workloads, the default static scheduling is more efficient:

use forkunion as fu;
use forkunion::prelude::*;

let topology = fu::Topology::new().expect("Failed to detect hardware topology");
let mut pool = fu::spawn(&topology, 4);
let mut data: Vec<usize> = (0..1000).collect();

(&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .for_each(|value, _task, _at| {
        println!("Value: {}", value);
    })?;

For dynamic work-stealing, use with_schedule with DynamicScheduler:

(&mut data[..])
    .into_par_iter()
    .with_schedule(&mut pool, DynamicScheduler)
    .for_each(|value, _task, _at| {
        *value *= 2;
    })?;

This easily composes with other iterator adaptors, like map, filter, and zip:

(&data[..])
    .into_par_iter()
    .filter(|&x| x % 2 == 0)
    .map(|x| x * x)
    .with_pool(&mut pool)
    .for_each(|value, _task, _at| {
        println!("Squared even: {}", value);
    })?;

For parallel reductions, ForkUnion provides Rayon-like convenience methods with automatic cache-aligned scratch allocation:

let data: Vec<u64> = (0..1_000_000).map(|i| i as u64).collect();

// Sum all elements with automatic scratch allocation
let total: u64 = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .sum()?;

// Count elements matching a predicate
let evens = (&data[..])
    .into_par_iter()
    .filter(|&x| x % 2 == 0)
    .with_pool(&mut pool)
    .count()?;

// Custom reduction (maximum)
let maximum = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .reduce(
        || 0u64,                                     // initial value
        |acc, value, _, _| *acc = (*acc).max(*value), // fold function
        |a, b| a.max(b)                              // combine function
    )?;

For manual control over scratch allocation, use reduce_with_scratch:

// Cache-line aligned wrapper to prevent false sharing
let mut scratch: Vec<CacheAligned<u64>> =
    (0..pool.threads_count()).map(|_| CacheAligned(0)).collect();

let total = (&data[..])
    .into_par_iter()
    .with_pool(&mut pool)
    .reduce_with_scratch(
        scratch.as_mut_slice(),
        |acc, value, _, _| acc.0 += *value,  // fold
        |a, b| a.0 += b.0                 // combine in-place
    )?;

Beyond reductions, the iterators offer short-circuiting searches - find_first and find_last for the deterministic lowest- or highest-index match, find_any for whichever match is found first with cooperative cancellation, and any / all for boolean predicates that stop the moment the answer is known. Fallible bodies get for_each_fallible and fold_with_scratch_fallible, which propagate the first error and signal the other workers to stop. Their two answers stay apart: the outer Result reports whether the dispatch ran, the inner one whether the caller’s own work succeeded, so the caller’s error type carries no constraint. This Rayon-style layer is Rust-only; C, C++, Zig, and Mojo expose the pool primitives for_threads, for_n, for_n_dynamic, and for_slices directly. Zig additionally mirrors Rust’s for_slices_mut as forSlicesMut, which hands each thread a disjoint sub-slice rather than the TasksRange its indices would form.