Rayon (Rust): Parallel Iterators vs Tokio Guide

Max WellsMax WellsFounder of Rustify

TL;DR: Rayon is Rust's data-parallelism library. It adds parallel versions of iterator methods, change .iter() to .par_iter() and Rayon automatically splits the work across all available CPU cores using a work-stealing thread pool. The API is identical to standard iterators, the parallelism is safe by construction (Rust's type system prevents data races), and the overhead is minimal for collections of more than a few thousand elements. Rayon is for CPU-bound parallelism; Tokio is for I/O-bound concurrency.


What Is Rayon?

Rayon is a data parallelism library that makes parallel iteration as simple as adding par_ to your iterator calls, no manual threading required.

# Cargo.toml
[dependencies]
rayon = "1"
use rayon::prelude::*;
 
fn main() {
    let numbers: Vec<i64> = (1..=1_000_000).collect();
 
    // Sequential; single thread
    let sum_seq: i64 = numbers.iter().sum();
 
    // Parallel; all CPU cores
    let sum_par: i64 = numbers.par_iter().sum();
 
    assert_eq!(sum_seq, sum_par);
    println!("Sum: {sum_par}");
}

Rayon uses a work-stealing thread pool sized to the number of logical CPU cores. Work is split recursively, large slices are divided in half until chunks are small enough to process serially. No explicit thread management, no channels, no locks for most use cases.


How Do Parallel Iterators Work?

par_iter() returns a ParallelIterator, it has the same adapter methods as Iterator but executes the chain in parallel across Rayon's thread pool.

use rayon::prelude::*;
 
fn main() {
    let words = vec!["hello", "world", "rust", "parallel", "fast"];
 
    // All the usual adapters work
    let upper: Vec<String> = words
        .par_iter()
        .filter(|w| w.len() > 4)
        .map(|w| w.to_uppercase())
        .collect();
 
    println!("{:?}", upper); // ["HELLO", "WORLD", "PARALLEL"]
 
    // Parallel sort
    let mut data: Vec<i32> = (0..1_000_000).rev().collect();
    data.par_sort(); // sorts in parallel; O(n log n) with parallelism
 
    // Parallel sum, min, max
    let sum: i64 = (1i64..=1_000_000).into_par_iter().sum();
    let max: Option<i32> = data.par_iter().copied().max();
}

What Operations Does Rayon Parallelize?

use rayon::prelude::*;
 
fn main() {
    let mut v: Vec<i32> = (0..1_000_000).collect();
 
    // Map + collect
    let doubled: Vec<i32> = v.par_iter().map(|&x| x * 2).collect();
 
    // Filter + map + collect
    let result: Vec<i32> = v.par_iter()
        .filter(|&&x| x % 2 == 0)
        .map(|&x| x * x)
        .collect();
 
    // Reduce
    let sum = v.par_iter().sum::<i32>();
    let product = v[0..10].par_iter().product::<i32>();
 
    // for_each; side effects (each closure runs in parallel)
    v.par_iter_mut().for_each(|x| *x += 1);
 
    // Sort
    v.par_sort();
    v.par_sort_by(|a, b| b.cmp(a)); // descending
    v.par_sort_by_key(|&x| x % 7);
 
    // Partition
    let (evens, odds): (Vec<i32>, Vec<i32>) = v.par_iter()
        .partition(|&&x| x % 2 == 0);
}

When Should You Use Rayon vs Tokio?

Rayon is for CPU-bound parallelism (computation). Tokio is for I/O-bound concurrency (waiting). They solve different problems and are often used together.

ScenarioTool
Image processing across many filesRayon
Database queriesTokio + SQLx
Number crunching / simulationsRayon
HTTP API callsTokio + Reqwest
Sorting or transforming large datasetsRayon
Waiting for timers / eventsTokio
Mixed: fetch files, then processTokio for I/O + Rayon for processing
// Combining both: async I/O + parallel CPU work
#[tokio::main]
async fn main() {
    // Fetch data asynchronously (I/O bound; use Tokio)
    let data: Vec<Vec<f64>> = fetch_datasets().await;
 
    // Process in parallel (CPU bound; use Rayon)
    let results: Vec<f64> = data
        .par_iter()
        .map(|dataset| compute_statistics(dataset))
        .collect();
}

Use tokio::task::spawn_blocking to run Rayon work from async code without blocking the Tokio executor.


Frequently Asked Questions

On an 8-core machine, compute-bound work can see up to 7–8× speedup for large collections. Smaller collections have overhead that reduces gains, typically Rayon is worth it for more than ~10,000 elements. Always benchmark your specific workload.

Yes, Rayon is safe by construction. The Send + Sync bounds on Rayon's API ensure only thread-safe types participate in parallel work, if your closure captures a non-Send type, the compiler rejects it. No data races are possible in safe Rayon code.

rayon::ThreadPoolBuilder::new()
    .num_threads(4)
    .build_global()
    .unwrap();

Or build a custom local thread pool with ThreadPoolBuilder::build(), useful to avoid interfering with the global pool.

Yes, par_iter() works on any type that implements IntoParallelIterator. Implement IntoParallelIterator for custom collections, or use par_bridge() to convert any Iterator to a ParallelIterator (with some overhead due to synchronization).


Sources


  • Iterator: Rayon extends the standard iterator API with parallel versions
  • Closure: All Rayon adapters take closures, they must be Send
  • Async/Await: Tokio handles I/O concurrency; Rayon handles CPU parallelism
  • Ownership: Rust's ownership system is what makes Rayon data-race-free
  • Bevy: Bevy and Rayon both lean on Rust's data-race guarantees for parallel workloads
  • Polars: Polars uses Rayon-style parallel execution under the hood for data processing

Keep Reading

Ready to Land a $80-120k Rust Job?