Data Parallelism with Rayon
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and Rayon 1.12, checked on 2026-10-06. For CPU-bound work over collections — parsing thousands of files, scoring every book against a query, numeric kernels — the Rayon crate turns sequential
iterator chains into parallel ones, usually by changing iter() to par_iter(). It handles the thread pool, splits the
work and balances it with work stealing, and the compiler’s Send/Sync checks guarantee the result has no data
races.
[dependencies]
rayon = "1.12"
Source: Rayon documentation
Parallel iterators
use rayon::prelude::*; adds par_iter() (items by &T), par_iter_mut() (by &mut T) and into_par_iter() (by
value) to slices, Vec, ranges, HashMap and other collections. Most adapters and consumers of Iterator have parallel
equivalents: map, filter, flat_map, enumerate, zip, sum, min_by_key, any, collect, for_each,
reduce…
use rayon::prelude::*;
fn score(title: &str, query: &str) -> u32 {
// stand-in for an expensive similarity function
title.to_lowercase().matches(query).count() as u32 * 10 + title.len() as u32 % 7
}
fn main() {
let titles: Vec<String> = (0..100_000).map(|i| format!("Book {i} about dune and desert {}", i % 13)).collect();
let best = titles
.par_iter() // was .iter()
.map(|t| (score(t, "dune"), t))
.max_by_key(|(s, _)| *s)
.map(|(s, t)| format!("{t} ({s})"));
println!("{best:?}");
let total_len: usize = titles.par_iter().map(String::len).sum();
let mut lens: Vec<usize> = titles.par_iter().map(String::len).collect(); // order preserved
lens.par_iter_mut().for_each(|n| *n *= 2);
println!("{total_len} {}", lens[0]);
}
Source: Rayon: parallel iterators
collect and indexed operations preserve the original order. Side-effecting closures (for_each(|x| println!(..))) run
in an unspecified order. Reductions must be associative for the result to be deterministic: sum of integers is, sum
of floats may differ in the last bits from the sequential result because the additions are grouped differently.
Sorting, chunks and the rest of the API
-
par_sort,par_sort_unstable,par_sort_by_keyon slices sort in parallel. -
par_chunks(n)andpar_chunks_mut(n)process fixed-size blocks — a good fit when each item is tiny and per-item overhead would dominate. -
par_bridge()turns an ordinary sequentialIterator(lines from a file, a channel) into a parallel one. -
with_min_len(n)stops Rayon from splitting work belownitems.
use rayon::prelude::*;
fn main() {
let mut years: Vec<u16> = (0..1_000_000u64).map(|i| (1500 + (i * 7919) % 525) as u16).collect();
years.par_sort_unstable();
println!("{} .. {}", years[0], years[years.len() - 1]);
let checksums: Vec<u32> = years.par_chunks(4096).map(|chunk| chunk.iter().map(|&y| u32::from(y)).sum()).collect();
println!("{} chunks", checksums.len());
}
Source: Rayon: ParallelSliceMut
join and scope
Below the iterators, Rayon has two primitives:
-
rayon::join(a, b)runs two closures, potentially in parallel, and returns both results — ideal for divide-and-conquer recursion; -
rayon::scope(|s| ...)lets yous.spawnany number of tasks that may borrow from the enclosing stack frame, all completed beforescopereturns (likestd::thread::scope, but on the pool).
fn count_words(texts: &[&str]) -> usize {
if texts.len() <= 2 {
return texts.iter().map(|t| t.split_whitespace().count()).sum();
}
let (left, right) = texts.split_at(texts.len() / 2);
let (a, b) = rayon::join(|| count_words(left), || count_words(right)); // fork-join
a + b
}
fn main() {
let texts = ["the spice must flow", "it is a truth", "universally acknowledged", "call me", "kindred spirits"];
println!("{}", count_words(&texts));
let mut summary = Vec::new();
rayon::scope(|s| {
let (left, right) = texts.split_at(2);
s.spawn(|_| println!("left half has {} texts", left.len()));
s.spawn(|_| println!("right half has {} texts", right.len()));
summary.push("scheduled"); // the scope may also use local state
});
println!("{summary:?}");
}
Source: Rayon: join
Work stealing
Rayon keeps one worker thread per CPU core (by default), each with a deque of tasks. join pushes one closure onto the
local deque and runs the other; idle workers steal from the back of busy workers' deques. Work is therefore split only
when there are idle threads to take it, which keeps overhead low and balances uneven workloads automatically.
Configuring the thread pool
The global pool is created lazily with one thread per available core; set RAYON_NUM_THREADS to override. For explicit
control — a dedicated pool for one subsystem, named threads, a smaller pool beside an async runtime — build one with
ThreadPoolBuilder and run work inside it with install:
use rayon::prelude::*;
fn main() -> Result<(), rayon::ThreadPoolBuildError> {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(2)
.thread_name(|i| format!("thumbnailer-{i}"))
.build()?;
let sizes = pool.install(|| (1..=8u32).into_par_iter().map(|n| n * 1024).collect::<Vec<_>>());
println!("{sizes:?} on {} threads", pool.current_num_threads());
Ok(())
}
Source: Rayon: ThreadPoolBuilder
Thread-safety requirements
Closures passed to parallel iterators run on several threads at once, so they must be Fn + Send + Sync and the items
must be Send. In practice: closures can read shared data freely, but cannot mutate captured variables (use a
reduction, par_iter_mut, an atomic, or a Mutex for that), and cannot capture Rc or RefCell. The compiler reports
violations as E0277 ("`Cell<u32>` cannot be shared between threads safely"):
use rayon::prelude::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn main() {
let isbns = ["9780441013593", "123", "9780141439518", "x"];
let invalid = AtomicUsize::new(0); // shared counter: atomic
let valid: Vec<&str> = isbns
.par_iter()
.filter(|s| {
let ok = s.len() == 13 && s.bytes().all(|b| b.is_ascii_digit());
if !ok {
invalid.fetch_add(1, Ordering::Relaxed);
}
ok
})
.copied()
.collect();
println!("{valid:?}, {} invalid", invalid.load(Ordering::Relaxed));
// let mut count = 0; isbns.par_iter().for_each(|_| count += 1); // would not compile: closure is FnMut, needs Fn
}
Source: Rayon: parallel iterators
Rayon or async?
They solve different problems:
| Rayon | async (tokio) | |
|---|---|---|
Work type |
CPU-bound: computation over data |
I/O-bound: waiting for network, disk, timers |
Concurrency |
one task per core, all busy |
thousands of tasks, mostly idle |
Blocking |
expected — tasks compute until done |
forbidden — a blocking task stalls its worker thread |
Typical use |
parsing, compression, image processing, numerics |
servers, clients, proxies |
Mixing them is common: an axum handler that needs to hash a password or resize a cover image should not do that CPU work
on the async runtime’s threads. Send it to tokio::task::spawn_blocking, or to a Rayon pool with a channel back to the
async task. See Async Runtimes & Tokio.
Parallel numeric loops
Rayon is the usual way to parallelise numeric code in Rust: element-wise operations over large arrays, Monte Carlo
simulations with one RNG per task, or blocked matrix operations. Crates such as ndarray (with its rayon feature) and
faer use it internally. Keep each task coarse enough (thousands of operations), avoid sharing writable state, and
benchmark against the sequential version — memory bandwidth, not cores, is often the limit. The Crossley et al. book was
consulted only to check which parallel numeric topics a reference should mention; see
Numbers & Numeric Computing.
use rayon::prelude::*;
fn main() {
let n = 2_000_000;
let xs: Vec<f64> = (0..n).map(|i| i as f64 / n as f64).collect();
// Approximate the integral of 4/(1+x^2) over [0, 1] (= pi) with the midpoint rule.
let dx = 1.0 / n as f64;
let pi: f64 = xs.par_iter().map(|x| 4.0 / (1.0 + (x + dx / 2.0).powi(2)) * dx).sum();
println!("{pi:.8}");
}
Source: Rayon: ParallelIterator::sum