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.