2.19. par - Parallel processing API
The rok::core::par module provides a data-parallel API. Its design is inspired by Rust's Rayon, and it runs on the same work-stealing scheduler as task::*.
Because use rok::core::**; applies implicitly, par::for_each, par::map and the rest are usable without any import; the module prefix par:: is still kept. Where the parallel intent should be visibly clear, writing use rok::core::par::*; explicitly is recommended.
2.19.1. Core API
The core API consists of six functions.
fn for_each<I: IntoIterator<Item = T>, T: Send>(iter: I, f: fn(T) + Send + Sync);
fn map<I: IntoIterator<Item = T>, T: Send, U: Send>(iter: I, f: fn(T) -> U + Send + Sync) -> List<U>;
fn reduce<T: Send + Sync>(iter: List<T>, init: T, f: fn(T, T) -> T + Send + Sync) -> T;
fn filter<T: Send>(iter: List<T>, pred: fn(T) -> bool + Send + Sync) -> List<T>;
fn join<T1: Send, T2: Send>(f1: fn() -> T1 + Send, f2: fn() -> T2 + Send) -> (T1, T2); // 2 in parallel
fn scope(f: fn(Scope)); // dynamic spawn
The f of for_each / map / reduce / filter is called by several workers at once, so it is + Send + Sync.
for_each/map/filterpreserve input order. The resultListofmap/filteris in the same order as the input.for_each/map/filtergive each item to a worker. Two items must not reach one collection that is notSync(2.13.9). Each call first looks for a collection reached from two items, following the items' parts and the elements of their collections to any depth, as a hand-off copy does, but not into collections of plain values (numbers,bool,str): if it finds one, the items run one after another on the calling thread, so nothing races and in-place changes stay as with workers, and a warning names the call site (once per site). Slices only read: items may share a list through slices of it (chunks of one big list), as long as no item holds the list itself; the elements a slice reaches are followed one by one. The check marks each collection the items reach in the object's header (about 15 ns per collection for one-list items and about 50 ns per collection on nested items, mostly the memory reads; items with no collection cost nothing): for many small items with little work each it can exceed the work. The warning says what to write instead: give each item its own values (.clone()a shared one before storing it), or share read-only data throughFrozen(2.16.4b) or anotherSynctype, which the check does not follow. A future among the items counts as its frame: two items holding one future run one at a time too.pardoes not hand its items over, so a future whose frame captures a collection cannot be an item (a compile error): spawn such futures as tasks instead (2.18.1). The result offiltershares the input's items.reducesplits the input into chunks, folds each chunk in parallel, then foldsinitand the chunk results in order. If the input is empty it returnsinit.- Children spawned inside a
scopeare all joined when the scope ends. parruns on the same work-stealing pool as tasks (2.18.3). A worker waiting insideparruns other tasks meanwhile, so callingparinside a task does not deadlock. TheScopepassed toscopeand its handle are:
struct Scope { /* list of jobs in the scope */ }
impl Scope {
fn spawn<T: Send>(self, f: fn() -> T + Send) -> ScopeHandle<T>;
}
struct ScopeHandle<T> { /* one spawned job */ }
impl<T> ScopeHandle<T> {
fn join(self) -> T; // waits until that job finishes
}
When scope ends it also waits for every job that was not joined.
Example
use rok::core::par::*;
let xs: List<i32> = (0..1000).collect();
par::for_each(xs, |x| { process(x) });
let squared: List<i32> = par::map(xs, |x| x * x);
let total: i32 = par::reduce(squared, 0, |a, b| a + b);
let (a, b) = par::join(
|| compute_a(),
|| compute_b(),
);
par::scope(|s| {
for chunk in chunks {
s.spawn(|| process_chunk(chunk));
}
}); // ← every spawn is guaranteed complete at the end of the scope
2.19.2. par::for_each vs Iterator
Sequential and parallel processing are distinguished by the form of the call.
xs.into_iter().for_each(f); // sequential (single thread)
par::for_each(xs, f); // parallel (worker pool)
Work is distributed data-parallel in chunks, and the work-stealing scheduler automatically balances unbalanced workloads.
2.19.3. par::scope - structured parallelism
par::scope is a structured parallel API that can spawn work dynamically.
par::scope(|s| {
let h1 = s.spawn(|| task1());
let h2 = s.spawn(|| task2());
// results of h1, h2 are usable (inside the scope)
// joined automatically at the end of the scope
});
This follows the structured concurrency form: the scope blocks until every child spawn has completed.
par::try_scope - cancel on the first error
For children that throw errors, use try_scope.
fn try_scope<E: Send>(f: fn(TryScope<E>) throws (E)) throws (E);
impl<E> TryScope<E> {
fn spawn<T: Send>(self, f: fn() throws (E) -> T + Send) -> TryScopeHandle<T, E>;
fn is_cancelled(self) -> bool;
}
impl<T, E: Send> TryScopeHandle<T, E> {
fn join(self) throws (E) -> T; // its own error, or the scope's first error if cancelled before starting
}
If one child throws, the scope is cancelled. Children not yet started are not run, and children already running run to completion. When all children have finished, try_scope rethrows that first error. A running child can check is_cancelled() to finish early.
try par::try_scope(|s| {
let a = s.spawn(|| try load("a"));
let b = s.spawn(|| try load("b"));
use_both(try a.join(), try b.join());
});
2.19.4. par::reduce - associativity required
The signature of par::reduce is as follows; using it involves some mathematical conditions.
fn reduce<T: Send + Sync>(iter: List<T>, init: T, f: fn(T, T) -> T + Send + Sync) -> T;
The combining function f must be associative for the result to be deterministic. init should preferably be the identity: 0 for +, 1 for *. For a non-associative function, such as floating-point summation accumulating large values, the result may be nondeterministic and needs explicit care.
let sum: i64 = par::reduce([1, 2, 3, 4, 5], 0, |a, b| a + b); //→ 15
// floating-point - the result may differ slightly
let total: f64 = par::reduce(values, 0.0, |a, b| a + b);
2.19.5. par::join - exactly two
Use par::join to run two jobs concurrently and receive the pair of results.
let (a, b) = par::join(
|| expensive_computation_1(),
|| expensive_computation_2(),
);
Two jobs are the most common fork-join form, so a dedicated function is provided. For N jobs, use par::for_each or par::scope.
2.19.6. SIMD / GPU integration
The par module centers on CPU data parallelism. SIMD multiversioning is applied automatically inside par::map according to target_feature; GPU acceleration is not part of the standard libraries; libraries reach it through rok::bare::mlir (1.9).
par::map(xs, |x| heavy_op(x)); // CPU work-stealing + automatic SIMD
2.19.7. Module mapping
The module location and internal strategy of each API:
| Item | Module | Strategy |
|---|---|---|
par::for_each | rok::core::par | chunk + work-steal |
par::map | rok::core::par | parallel collect |
par::reduce | rok::core::par | tree reduction |
par::filter | rok::core::par | parallel collect, then dedup |
par::join | rok::core::par | 2-task work-steal |
par::scope | rok::core::par | spawn handle + auto-join |
Internally it is one layer on top of task::spawn, sharing the same scheduler.