Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
129: rayon for Data Parallelism
You've probably noticed that Rust's ownership model makes concurrency "fearless," but actually splitting a massive task across all your CPU cores manually using std::thread is still a lot of boilerplate. You have to handle joining, slicing your data into chunks, and dealing with Arc or Mutex if you aren't careful. It's tedious.
I ran into this last week while processing a huge set of simulation data. I had a vector of about 50 million floats and needed to perform some heavy math on each one. I started with a standard iterator, thinking it would be "fast enough."
The Single-Core Slog
fn main() {
let data: Vec = (0..50_000_000).map(|x| x as f64).collect();
let start = std::time::Instant::now();
let result: f64 = data.iter()
.map(|x| x.sin().powi(2) + x.cos().powi(2)) // Heavy-ish math
.sum();
println!("Result: {}, took: {:?}", result, start.elapsed());
}
On my machine, this takes a few seconds. Not a disaster, but it's only using one core. If I open my activity monitor, I see one CPU spike to 100% while the other seven just sit there. That feels like a waste of hardware. I want all of them working.
Dropping in Rayon
This is where rayon comes in. It’s the gold standard for data parallelism in Rust. The goal of rayon is to make parallelism feel like a natural extension of iterators. I added rayon = "1.10" to my Cargo.toml and tried the most basic swap: changing .iter() to .par_iter().
use rayon::prelude::*; // This trait is key
fn main() {
let data: Vec = (0..50_000_000).map(|x| x as f64).collect();
let start = std::time::Instant::now();
// Just one word change: iter -> par_iter
let result: f64 = data.par_iter()
.map(|x| x.sin().powi(2) + x.cos().powi(2))
.sum();
println!("Result: {}, took: {:?}", result, start.elapsed());
}
I ran this, and the time dropped significantly—nearly 6x faster on my 8-core machine. Rayon is doing "work-stealing" under the hood. It recursively splits the vector into halves until the chunks are small enough to be handled efficiently, and then it distributes those chunks across a thread pool. I didn't have to manage a single thread handle.
Hitting the Borrow Checker Wall
Now, I wanted to do something a bit more complex. Instead of just summing, I wanted to filter the results and push them into a shared Vec. I tried this:
let mut results = Vec::new();
data.par_iter().for_each(|x| {
if *x > 10.0 {
results.push(*x); // Error!
}
});
Immediately, the compiler yelled at me. It makes sense: results.push() requires a mutable reference, and you can't have multiple threads mutating the same Vec simultaneously without synchronization. I could have wrapped the Vec in a Mutex, but that would create a massive bottleneck because every thread would be fighting for the lock, effectively turning my parallel code back into serial code.
The "Rayon way" is to avoid shared mutable state and instead lean on collect(). Since par_iter() returns a ParallelIterator, I can just collect the results back into a collection. Rayon handles the complex logic of merging the partial results from different threads in the correct order.
let results: Vec = data.par_iter()
.filter(|&&x| x > 10.0)
.map(|x| x * 2.0)
.collect(); // Rayon handles the thread-safe merging here
That's the mental shift you have to make. Don't try to "push" data into a shared bucket from inside a parallel loop. Instead, describe the transformation as a pipeline and let collect() or reduce() assemble the final piece. It keeps the code clean and prevents you from fighting with Arc<Mutex<T>> everywhere.
📋 Practical Task
Exercise: Parallelizing a Large-Scale Prime Filter
You have a dataset of 10 million integers. Your task is to filter this list to keep only the prime numbers and then calculate the sum of those primes.
Because primality testing is CPU-intensive, a serial iterator is too slow. Implement the following requirements using rayon:
- Create a
Vec<u64>containing numbers from 2 to 10,000,000. - Implement a helper function
is_prime(n: u64) -> bool. - Use
par_iter()to filter the vector for primes. - Use
sum()to calculate the total of all identified primes. - Print the final sum and the time elapsed.
Ensure you include rayon = "1.10" in your dependencies and import rayon::prelude::* to bring the parallel iterator traits into scope.
There are no comments for now.