rayon
rayon turns sequential iterators into parallel ones with a work-stealing thread pool. It shines on CPU-bound data processing without manual thread management.
Recipe
[dependencies]
rayon = "1.10"use rayon::prelude::*;
fn main() {
let sum: u64 = (0..1_000_000u64).into_par_iter().sum();
assert_eq!(sum, 499_999_500_000);
}When to reach for this:
- Batch transforms on large
Vecs - Parallel search or aggregation
- Offline ETL, image processing, scientific workloads
Working Example
use rayon::prelude::*;
#[derive(Debug)]
struct Row { value: i32 }
fn normalize(rows: &mut [Row]) {
rows.par_iter_mut().for_each(|r| {
r.value = r.value.saturating_mul(2);
});
}
fn main() {
let mut data: Vec<Row> = (0..10_000).map(|i| Row { value: i }).collect();
normalize(&mut data);
assert_eq!(data[100].value, 200);
}What this demonstrates:
par_iter/par_iter_mutmirror std iterators- Rayon schedules work across threads
- No manual
Mutexfor independent elements
Deep Dive
Parallel Patterns
| Method | Use |
|---|---|
par_iter().map() | Transform collections |
par_chunks() | Slice-based parallelism |
join | Recursive divide-and-conquer |
ThreadPoolBuilder | Custom thread count |
With Tokio
Do not run rayon inside async tasks without spawn_blocking. Pattern:
let out = tokio::task::spawn_blocking(move || {
data.par_iter().map(|x| x * 2).collect::<Vec<_>>()
}).await?;Gotchas
- Small inputs - thread overhead dominates. Fix: parallel only above threshold (e.g. 10k items).
- Shared mutation -
par_iter_mutneeds disjoint access. Fix: use channels or map-reduce. - IO in rayon - blocks pool threads. Fix: keep IO on Tokio, CPU on rayon.
- Non-Send types -
Rcacross threads fails. Fix:Arcor owned data per task. - Order not preserved -
for_eachis unordered. Fix:mapthencollectif order matters.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
std::thread::scope | Few explicit threads | Large data parallelism |
Tokio spawn_blocking | Occasional CPU in server | Heavy parallel maps |
ndarray + BLAS | Numeric linear algebra | Generic collections |
FAQs
Does rayon use all CPUs?
Default pool size matches logical cores. Configure with ThreadPoolBuilder.
Safe with Rust's ownership?
Yes. Rayon APIs enforce Send and borrow rules at compile time.
Can I nest par_iter?
Possible but often oversubscribes CPUs. Prefer flat parallel pipeline.
Polars vs rayon?
Polars has internal parallelism. Use rayon for custom Rust logic outside DataFrames.
Related
- Data parallelism with Rayon
- tokio - async boundary
- Performance Skill
- Polars - dataframe parallelism
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.