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"

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]);
}

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_key on slices sort in parallel.

  • par_chunks(n) and par_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 sequential Iterator (lines from a file, a channel) into a parallel one.

  • with_min_len(n) stops Rayon from splitting work below n items.

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());
}

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 you s.spawn any number of tasks that may borrow from the enclosing stack frame, all completed before scope returns (like std::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(())
}

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
}

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}");
}