What are the trade-offs between rayon::iter::IntoParallelRefIterator and IntoParallelRefMutIterator for parallel borrowing?
IntoParallelRefIterator provides parallel iteration over shared references (&T), enabling read-only access from multiple threads safely, while IntoParallelRefMutIterator provides parallel iteration over mutable references (&mut T), enabling concurrent modification of elements. The choice between them determines whether parallel operations can modify data and affects what operations are available on elements.
Parallel Iteration in Rayon
use rayon::prelude::*;
fn parallel_basics() {
let data = vec
![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Serial iteration (standard library)
let sum: i32 = data.iter().sum();
// Parallel iteration (rayon)
let parallel_sum: i32 = data.par_iter().sum();
// par_iter() uses IntoParallelRefIterator
// It borrows each element as &i32 across multiple threads
}Rayon's parallel iterators split work across threads using work-stealing for load balancing.
IntoParallelRefIterator - Shared References
use rayon::prelude::*;
fn shared_references() {
let data = vec
![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// IntoParallelRefIterator is implemented for types that can provide
// parallel iteration over shared references (&T)
// par_iter() returns ParallelIterator<Item = &T>
let sum: i32 = data.par_iter().sum();
// Each element is borrowed immutably
// Multiple threads can read simultaneously
// No data races possible because no mutation
// Can also use into_par_iter() on a reference:
let sum2: i32 = (&data).into_par_iter().sum();
assert_eq!(sum, sum2);
}IntoParallelRefIterator enables parallel read-only access through shared references.
IntoParallelRefMutIterator - Mutable References
use rayon::prelude::*;
fn mutable_references() {
let mut data = vec
![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// IntoParallelRefMutIterator is implemented for types that can provide
// parallel iteration over mutable references (&mut T)
// par_iter_mut() returns ParallelIterator<Item = &mut T>
data.par_iter_mut().for_each(|x| {
*x *= 2; // Modify each element in parallel
});
assert_eq!(data, vec
![2, 4, 6, 8, 10, 12, 14, 16, 18, 20]);
// Each element is borrowed mutably
// Only one thread can access each element at a time
// Safe because each thread gets a unique &mut T
}IntoParallelRefMutIterator enables parallel modification through mutable references.
The Trait Signatures
use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator};
fn trait_signatures() {
// IntoParallelRefIterator trait:
// trait IntoParallelRefIterator<'data> {
// type Iter: ParallelIterator<Item = Self::Item>;
// type Item: Send + 'data;
//
// fn par_iter(&'data self) -> Self::Iter;
// }
// IntoParallelRefMutIterator trait:
// trait IntoParallelRefMutIterator<'data> {
// type Iter: ParallelIterator<Item = Self::Item>;
// type Item: Send + 'data;
//
// fn par_iter_mut(&'data mut self) -> Self::Iter;
// }
// Key differences:
// - par_iter takes &self (shared borrow)
// - par_iter_mut takes &mut self (exclusive borrow)
// - Item is &T vs &mut T
}The trait signatures encode the borrowing semantics in the type system.
What Operations Are Available
use rayon::prelude::*;
fn available_operations() {
// With IntoParallelRefIterator (par_iter):
let data = vec
![1, 2, 3, 4, 5];
// Read-only operations
let sum: i32 = data.par_iter().sum();
let product: i32 = data.par_iter().product();
let max: Option<&i32> = data.par_iter().max();
let cloned: Vec<i32> = data.par_iter().cloned().collect();
// Can read but not modify
data.par_iter().for_each(|x| {
println!("{}", x); // OK: reading
// *x += 1; // ERROR: cannot mutate &T
});
// With IntoParallelRefMutIterator (par_iter_mut):
let mut data = vec
![1, 2, 3, 4, 5];
// Can modify elements
data.par_iter_mut().for_each(|x| {
*x *= 2; // OK: modifying &mut T
});
// Can still do read operations
let sum: i32 = data.par_iter_mut().map(|x| *x).sum();
}par_iter enables read-only operations; par_iter_mut enables modification.
Ownership and Borrowing Rules
use rayon::prelude::*;
fn ownership_rules() {
let mut data = vec
![1, 2, 3, 4, 5];
// par_iter borrows data immutably
let sum: i32 = data.par_iter().sum();
// data is still usable after par_iter
println!("Sum: {}, Data: {:?}", sum, data);
// par_iter_mut borrows data mutably
data.par_iter_mut().for_each(|x| *x += 1);
// During par_iter_mut, nothing else can access data
// After par_iter_mut completes, data is usable again
// This would fail:
// let r = &data[0]; // immutable borrow
// data.par_iter_mut().for_each(|x| *x += 1); // mutable borrow conflicts
// This works:
let sum = data.par_iter().sum(); // immutable borrow ends
data.par_iter_mut().for_each(|x| *x += 1); // then mutable borrow
}Standard Rust borrowing rules apply: shared borrows can coexist, mutable borrows are exclusive.
Concurrency Model
use rayon::prelude::*;
fn concurrency_model() {
let mut data: Vec<i32> = (0..1000).collect();
// par_iter: Multiple threads read simultaneously
// Thread 1: reads data[0..250]
// Thread 2: reads data[250..500]
// Thread 3: reads data[500..750]
// Thread 4: reads data[750..1000]
// All safe because no mutation
let count = data.par_iter().filter(|&&x| x > 500).count();
// par_iter_mut: Multiple threads write simultaneously
// Thread 1: modifies data[0..250]
// Thread 2: modifies data[250..500]
// Thread 3: modifies data[500..750]
// Thread 4: modifies data[750..1000]
// Safe because each thread has exclusive access to its portion
data.par_iter_mut().for_each(|x| *x *= 2);
// Key: Each element is accessed by exactly one thread
// No two threads ever access the same element
}Rayon guarantees exclusive access per element in par_iter_mut through non-overlapping slices.
Send Requirement
use rayon::prelude::*;
use std::rc::Rc;
fn send_requirement() {
// Both traits require elements to be Send
// Because elements are sent between threads
// This works: i32 is Send
let data = vec
![1, 2, 3, 4, 5];
data.par_iter().sum(); // OK
// This fails: Rc<T> is not Send
// let data: Vec<Rc<i32>> = vec
![Rc::new(1), Rc::new(2)];
// data.par_iter().for_each(|x| {}); // ERROR: Rc is not Send
// Use Arc instead for parallel iteration
use std::sync::Arc;
let data: Vec<Arc<i32>> = vec
![Arc::new(1), Arc::new(2)];
data.par_iter().for_each(|x| {
println!("{}", *x); // OK: Arc is Send
});
}Both traits require Send because data is transferred across thread boundaries.
Performance Characteristics
use rayon::prelude::*;
fn performance() {
let data: Vec<i32> = (0..1_000_000).collect();
// par_iter: Read-only, minimal synchronization
// - No need to synchronize reads
// - Only synchronization is work-stealing
// - Cache-friendly for read-heavy operations
let sum: i32 = data.par_iter().sum();
// par_iter_mut: Write, still minimal synchronization
// - Each thread writes to distinct elements
// - No data races, no locks needed
// - False sharing possible if threads write to nearby elements
let mut data: Vec<i32> = (0..1_000_000).collect();
data.par_iter_mut().for_each(|x| *x *= 2);
// Performance considerations:
// 1. Work must be substantial enough to offset parallelism overhead
// 2. Small collections may be faster with serial iteration
// 3. Expensive per-element operations benefit most
}Both approaches have minimal synchronization overhead; the key is sufficient work per element.
When to Use Each
use rayon::prelude::*;
fn use_cases() {
// Use par_iter (IntoParallelRefIterator) when:
// - Computing derived values (sum, count, filter)
// - Searching for elements
// - Transforming into new collections
// - Read-only analysis
let data = vec
![1, 2, 3, 4, 5];
let sum: i32 = data.par_iter().sum();
let doubled: Vec<i32> = data.par_iter().map(|&x| x * 2).collect();
// Use par_iter_mut (IntoParallelRefMutIterator) when:
// - In-place modification of elements
// - Updating values based on computation
// - Filling a buffer in parallel
let mut data = vec
![0; 1000];
data.par_iter_mut().enumerate().for_each(|(i, x)| {
*x = i as i32 * i as i32; // Compute square
});
}Use par_iter for read-only operations, par_iter_mut for in-place modification.
Nested Collections
use rayon::prelude::*;
fn nested_collections() {
let matrix: Vec<Vec<i32>> = vec
![
vec
![1, 2, 3],
vec
![4, 5, 6],
vec
![7, 8, 9],
];
// Outer par_iter, inner par_iter
let sum: i32 = matrix.par_iter()
.map(|row| row.par_iter().sum::<i32>())
.sum();
// Flat par_iter
let flat_sum: i32 = matrix.par_iter()
.flatten()
.sum();
// Mutable nested iteration
let mut matrix = matrix;
matrix.par_iter_mut().for_each(|row| {
row.par_iter_mut().for_each(|val| {
*val *= 2;
});
});
}Both iterators support nested parallel iteration for multi-dimensional data.
Custom Types
use rayon::prelude::*;
// Implement IntoParallelRefIterator and IntoParallelRefMutIterator
// for custom types that support parallel iteration
struct Container {
data: Vec<i32>,
}
impl<'data> IntoParallelRefIterator<'data> for Container {
type Iter = rayon::slice::Iter<'data, i32>;
type Item = &'data i32;
fn par_iter(&'data self) -> Self::Iter {
self.data.par_iter()
}
}
// Similarly for IntoParallelRefMutIterator
fn custom_types() {
let container = Container { data: vec
![1, 2, 3, 4, 5] };
let sum: i32 = container.par_iter().sum();
}Custom types can implement these traits by delegating to their parallel iterators.
Comparison Table
use rayon::prelude::*;
fn comparison() {
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// β Aspect β IntoParallelRefIterator β IntoParallelRefMutIterator β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
// β Method β par_iter() β par_iter_mut() β
// β Input β &self β &mut self β
// β Item type β &T β &mut T β
// β Access β Read-only β Read-write β
// β Self after β Still usable β Still usable β
// β Concurrent access β Multiple readers β Multiple writers (distinct)β
// β Use case β Computing sums, counts β In-place modification β
// β Send requirement β T: Send β T: Send β
// β Synchronization β Work-stealing only β Work-stealing only β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
}Complete Example
use rayon::prelude::*;
fn main() {
// Example 1: Read-only parallel operations
let data = vec
![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Sum using par_iter
let sum: i32 = data.par_iter().sum();
println!("Sum: {}", sum);
// Filter and collect using par_iter
let evens: Vec<&i32> = data.par_iter().filter(|&&x| x % 2 == 0).collect();
println!("Evens: {:?}", evens);
// Map to new values using par_iter
let squares: Vec<i32> = data.par_iter().map(|&x| x * x).collect();
println!("Squares: {:?}", squares);
// Example 2: In-place modification using par_iter_mut
let mut data = vec
![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Double each element in place
data.par_iter_mut().for_each(|x| *x *= 2);
println!("Doubled: {:?}", data);
// Conditional modification
data.par_iter_mut().for_each(|x| {
if *x > 10 {
*x = 0; // Reset large values
}
});
println!("After conditional: {:?}", data);
// Example 3: Complex parallel transformation
let mut matrix: Vec<Vec<i32>> = (0..5)
.map(|i| (0..5).map(|j| i * 5 + j).collect())
.collect();
println!("Matrix before: {:?}", matrix);
// Modify in place
matrix.par_iter_mut().for_each(|row| {
row.par_iter_mut().for_each(|val| {
*val = *val * 2 + 1;
});
});
println!("Matrix after: {:?}", matrix);
// Example 4: Parallel computation with early exit pattern
let data: Vec<i32> = (0..1000).collect();
// Find first element matching condition
let first_match = data.par_iter().find_first(|&&x| x > 500);
println!("First > 500: {:?}", first_match);
// Check if any element matches
let any_match = data.par_iter().any(|&&x| x > 999);
println!("Any > 999: {}", any_match);
}Summary
use rayon::prelude::*;
fn summary() {
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// β Aspect β par_iter β par_iter_mut β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
// β Trait β IntoParallelRefIteratorβ IntoParallelRefMutIteratorβ
// β Borrow type β &T (shared) β &mut T (exclusive) β
// β Self borrow β &self β &mut self β
// β Modification β No β Yes β
// β Operations β Read, filter, map β Write, transform in-placeβ
// β Thread safety β Safe (read-only) β Safe (distinct elements)β
// β Use when β Computing results β Modifying data β
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Key points:
// 1. par_iter borrows immutably (&self), par_iter_mut borrows mutably (&mut self)
// 2. par_iter gives &T, par_iter_mut gives &mut T
// 3. Both are safe because Rayon splits work into non-overlapping chunks
// 4. Both require T: Send for thread safety
// 5. Use par_iter for read-only, par_iter_mut for modification
// 6. Neither requires explicit locks - synchronization is handled by Rayon
}Key insight: IntoParallelRefIterator and IntoParallelRefMutIterator encode Rust's ownership model into parallel iteration. par_iter() provides shared references for concurrent read operations, while par_iter_mut() provides mutable references for concurrent write operations. Rayon's safety guarantee is that each element is processed by exactly one thread, preventing data races without explicit locking. The choice between them follows the same reasoning as choosing between &T and &mut T in serial code: use shared references for reading, mutable references for writing.
