---
title: "22.5 Data parallelism with rayon"
description: "Turning a sequential iterator into a parallel one by changing iter to par_iter, and the ecosystem victory lap of adding one dependency."
url: "https://learnrust.net/chapter-22/data-parallelism-with-rayon/"
last_updated: "2026-06-13"
---

# 22.5 Data parallelism with rayon

The last four lessons built concurrency from the ground up: threads, channels, locks, the traits that keep it all safe. That's the machinery, and it's worth understanding. But for one extremely common job, parallelizing work over a collection, you don't have to touch any of it. The **`rayon`** crate turns a sequential iterator into a parallel one by changing a single method name, and it handles the threads, the work distribution, and the joining for you. This lesson is a victory lap: the chapter-19 iterators you already know, suddenly running on every core at once.

It's also your second real dependency, after `rand` back in chapter 7. Adding a crate to do something genuinely hard in one line is the Rust ecosystem at its best, and worth experiencing.

## A sequential baseline

Here's a computation that does real work for each item: summing the squares of a range, but pretend each square is expensive. Sequentially, with the iterators from chapter 19:

```rust
fn main() {
    let total: u64 = (1..=1_000_000u64)
        .map(|n| n * n)
        .sum();

    println!("{total}");
}
```

```
333333833333500000
```

This runs on one core, one item after another. For a million cheap multiplications that's fine, but if the per-item work were heavy (processing images, hashing, simulating), doing them one at a time would leave most of your machine's cores idle. We'd like to split the items across all cores.

## Add the dependency

`rayon` lives on crates.io. Add it the way you added `rand` (lesson [7.9](https://learnrust.net/chapter-7/adding-a-dependency-random-numbers/)):

```bash
$ cargo add rayon
```

```
    Updating crates.io index
      Adding rayon v1.10.0 to dependencies
```

That writes `rayon = "1.10"` into `Cargo.toml`'s `[dependencies]`. (The exact version will be whatever's current; the `1.10` line is what these examples were written against.) Now the one import that unlocks everything:

```rust
use rayon::prelude::*;
```

`rayon`'s prelude adds parallel-iterator methods to standard collections and ranges, the same way the standard prelude gives you `Vec` and `String` without imports.

## Change one word

Here is the entire payoff. Take the sequential program and change `.map(...)` 's driver from a normal iterator to a parallel one, by swapping the *entry point*. For a range or collection you'd reach `.iter()`; the parallel version is **`.par_iter()`**. For an owned range, the parallel constructor reads `(1..=n).into_par_iter()`:

```rust
use rayon::prelude::*;

fn main() {
    let total: u64 = (1..=1_000_000u64)
        .into_par_iter()
        .map(|n| n * n)
        .sum();

    println!("{total}");
}
```

```
333333833333500000
```

Same answer. But `into_par_iter` instead of the implicit sequential iteration means `rayon` splits the range into chunks, runs the `map` and `sum` on multiple threads across your CPU cores, and combines the partial results, all behind that one method. The `.map(|n| n * n).sum()` is *identical* to the sequential version; you wrote the same pipeline you learned in chapter 19, and only the entry point changed. On a multi-core machine with genuinely expensive per-item work, this finishes in a fraction of the sequential time.

For a `Vec` it's exactly as smooth: `v.iter()` becomes `v.par_iter()`, and the rest of the chain, `filter`, `map`, `sum`, `collect`, works unchanged.

```rust
use rayon::prelude::*;

fn main() {
    let numbers: Vec<u64> = (1..=20).collect();

    let sum_of_even_squares: u64 = numbers
        .par_iter()
        .filter(|&&n| n % 2 == 0)
        .map(|&n| n * n)
        .sum();

    println!("{sum_of_even_squares}");
}
```

```
1540
```

`par_iter()` in place of `iter()`, and this filter-map-sum now runs in parallel. Everything else is the chapter-19 pipeline you already know.

> **Why this is safe, and not magic**
>
> `rayon` can parallelize your iterator without you writing a single lock because of `Send` and `Sync` (lesson [22.4](https://learnrust.net/chapter-22/send-and-sync/)). It requires the closures and data in your pipeline to be `Send`/`Sync`, and the compiler checks that for you. If your `map` closure tried to mutate shared non-thread-safe state, it wouldn't compile, the same fearless-concurrency guarantee, now protecting code you didn't even realize was concurrent. `rayon` isn't bypassing Rust's safety; it's standing on top of it. That's why "just change `iter` to `par_iter`" is actually safe advice and not a footgun.

## When it helps, and when it doesn't

Parallelism isn't free: splitting work across threads and recombining has overhead. For the filter-map-sum over twenty tiny numbers above, the parallel version is almost certainly *slower* than sequential, the coordination costs more than the work saved. `rayon` shines when there are *many* items and the per-item work is *non-trivial*: processing thousands of files, transforming a large image, running a simulation step over a big grid. The rule of thumb: reach for `par_iter` when you have a real amount of independent, CPU-bound work, and measure (in release mode, lesson [19.7](https://learnrust.net/chapter-19/loops-vs-iterators/)) to confirm it actually helped. For small or I/O-bound work, plain sequential iterators are the right call.

> **Best practice**
>
> When you have a CPU-bound computation over many independent items, write it as an ordinary chapter-19 iterator pipeline first, get it correct and readable, then, if profiling shows it's a bottleneck, switch the entry iterator to `par_iter`/`into_par_iter` and measure the speedup. Don't parallelize speculatively: for small inputs or I/O-bound work, the coordination overhead can make `rayon` slower. The beauty is that the change is one word, so there's no reason to parallelize before you know you need to.

## Quiz time

**Question #1**

What single change turns a sequential iterator pipeline into a parallel one with `rayon`, and what stays the same?

<details class="solution">
<summary>Show solution</summary>

You change the *entry point* of the pipeline: `.iter()` becomes `.par_iter()` (or `.into_par_iter()` for owned iteration), after bringing in `use rayon::prelude::*`. Everything else, the `map`, `filter`, `sum`, `collect`, stays exactly the same. `rayon` splits the work across threads and recombines the results behind that one method; the pipeline you wrote in chapter 19 is unchanged.

</details>

**Question #2**

How can `rayon` parallelize your code safely without you writing any locks?

<details class="solution">
<summary>Show solution</summary>

It relies on `Send`/`Sync` (lesson 22.4): `rayon`'s parallel-iterator methods require the closures and data to be thread-safe, and the compiler checks those bounds. If your pipeline tried to do something unsafe across threads (mutating shared non-thread-safe state), it wouldn't compile. So `rayon` builds on Rust's existing fearless-concurrency guarantee rather than bypassing it, the safety is the same one protecting the rest of the chapter.

</details>

**Question #3**

When is `par_iter` *not* worth using?

<details class="solution">
<summary>Show solution</summary>

When there are few items or the per-item work is trivial (the overhead of splitting and recombining outweighs the savings), or when the work is I/O-bound rather than CPU-bound (parallelism helps with CPU work, not waiting). For small or light workloads, parallel can be slower than sequential. Use `par_iter` for large amounts of independent, CPU-heavy work, and measure in release mode to confirm it helped.

</details>

That completes the concurrency chapter. The summary and quiz ([22.x](https://learnrust.net/chapter-22/chapter-22-summary-and-quiz/)) tie threads, channels, shared state, the marker traits, and `rayon` together. After it, chapter 23 turns to a different flavor of concurrency, *async*, the model for handling thousands of waiting tasks (network connections, timers) without a thread for each, and it's where Rust is honest about showing some seams.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
