How does rayon::iter::IntoParallelRefIterator::par_iter differ from into_par_iter for borrowing ownership?
par_iter creates a parallel iterator that borrows each element from the collection, yielding references (&T) that can be read concurrently, while into_par_iter consumes the collection and yields owned values (T) that can be mutated or moved. The fundamental distinction is ownership: par_iter preserves the original collection for later use but restricts operations to read-only access through references, whereas into_par_iter takes ownership of the collection, allowing full control over elements at the cost of destroying the original.
The Ownership Distinction
use rayon::prelude::*;
fn ownership_basics() {
let data = vec![1, 2, 3, 4, 5];
// par_iter: borrows elements, yields &i32
// Collection is preserved after the operation
let sum: i32 = data.par_iter().sum();
println!("Sum: {}", sum);
println!("Data still exists: {:?}", data); // data is still valid
// into_par_iter: takes ownership, yields i32
// Collection is consumed
let product: i32 = data.into_par_iter().product();
println!("Product: {}", product);
// data is moved - can't use it anymore
// println!("Data: {:?}", data); // ERROR: data moved
}par_iter borrows and preserves; into_par_iter consumes and transforms.
Borrowing with par_iter
use rayon::prelude::*;
fn par_iter_borrowing() {
let data = vec![String::from("hello"), String::from("world")];
// par_iter yields &String references
let lengths: Vec<usize> = data.par_iter()
.map(|s| s.len())
.collect();
// Original collection is still available
println!("Original: {:?}", data);
println!("Lengths: {:?}", lengths);
// Can use data again in another parallel operation
let upper: Vec<String> = data.par_iter()
.map(|s| s.to_uppercase())
.collect();
println!("Uppercase: {:?}", upper);
}par_iter yields &T, allowing multiple parallel operations on the same collection.
Taking Ownership with into_par_iter
use rayon::prelude::*;
fn into_par_iter_ownership() {
let data = vec![String::from("hello"), String::from("world")];
// into_par_iter yields owned String values
let modified: Vec<String> = data.into_par_iter()
.map(|mut s| {
s.push_str("!");
s // Return owned String
})
.collect();
// data is consumed, can't use it anymore
// But we now own the modified strings
println!("Modified: {:?}", modified);
// Can transform the data completely
let numbers: Vec<usize> = modified.into_par_iter()
.map(|s| s.len())
.collect();
println!("Numbers: {:?}", numbers);
}into_par_iter yields T, allowing mutation and transformation of owned values.
When to Use par_iter
use rayon::prelude::*;
fn when_par_iter() {
let data = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
// Use par_iter when:
// 1. You need to reuse the collection after the operation
let count: usize = data.par_iter().map(|v| v.len()).sum();
println!("Total elements: {}", count);
// 2. You only need to read elements
let flattened: Vec<&i32> = data.par_iter()
.flat_map(|v| v.iter())
.collect();
println!("Flattened: {:?}", flattened);
// 3. The collection is borrowed from somewhere else
fn process_data(data: &Vec<Vec<i32>>) -> Vec<i32> {
data.par_iter()
.map(|v| v.iter().sum())
.collect()
}
// 4. Collection is expensive to create/clone
let expensive_data = create_expensive_collection();
// Process multiple times without recreating
let sum: i32 = expensive_data.par_iter().sum();
let max: Option<&i32> = expensive_data.par_iter().max();
// expensive_data is still usable
fn create_expensive_collection() -> Vec<i32> {
(0..1000000).collect()
}
}Use par_iter for read-only operations or when the collection must be preserved.
When to Use into_par_iter
use rayon::prelude::*;
fn when_into_par_iter() {
// Use into_par_iter when:
// 1. You want to mutate elements in place
let mut data = vec![1, 2, 3, 4, 5];
data.into_par_iter()
.for_each(|mut x| {
x *= 2;
// Can mutate owned value
});
// Note: into_par_iter consumes, so use par_iter_mut for in-place
// 2. You need to transform element types
let strings = vec!["1", "2", "3"];
let numbers: Vec<i32> = strings.into_par_iter()
.map(|s| s.parse().unwrap())
.collect();
// strings is consumed, numbers has new type
// 3. You want to move elements into new structures
let items = vec!["a", "b", "c"];
let owned: Vec<String> = items.into_par_iter()
.map(|s| s.to_string())
.collect();
// 4. Collection is temporary anyway
let processed: Vec<i32> = (0..100).into_par_iter()
.map(|x| x * x)
.collect();
// No need to keep the range
// 5. Working with owned values from other parallel operations
let result: Vec<String> = (0..10)
.into_par_iter()
.map(|i| format!("item_{}", i))
.collect();
}Use into_par_iter when you need ownership or the collection is temporary.
par_iter_mut for In-Place Mutation
use rayon::prelude::*;
fn par_iter_mut_example() {
// There's a third option: par_iter_mut
// This yields &mut T, allowing in-place mutation
// while preserving the collection
let mut data = vec![1, 2, 3, 4, 5];
// par_iter_mut yields &mut i32
data.par_iter_mut()
.for_each(|x| {
*x *= 2;
});
// Collection is preserved and modified in place
println!("Modified: {:?}", data);
// Compare to into_par_iter for mutation:
// into_par_iter: consumes collection, yields owned values
// par_iter_mut: preserves collection, yields mutable references
// Example: parallel in-place sorting
let mut nested = vec![vec![3, 1, 2], vec![6, 5, 4], vec![9, 8, 7]];
nested.par_iter_mut()
.for_each(|v| v.sort());
println!("Sorted: {:?}", nested);
}par_iter_mut is the middle ground: mutable access without consuming the collection.
Type Signatures Compared
use rayon::prelude::*;
fn type_signatures() {
// par_iter
// Takes: &self
// Yields: &T
// Collection: preserved (borrowed)
// par_iter_mut
// Takes: &mut self
// Yields: &mut T
// Collection: preserved (mutably borrowed)
// into_par_iter
// Takes: self
// Yields: T
// Collection: consumed (moved)
let data = vec![String::from("hello"), String::from("world")];
// par_iter: &String
let refs: Vec<&String> = data.par_iter().collect();
// data still valid
// par_iter_mut: &mut String
let mut data2 = vec![String::from("hello")];
data2.par_iter_mut().for_each(|s| s.push('!'));
// data2 still valid, modified in place
// into_par_iter: String (owned)
let data3 = vec![String::from("hello")];
let owned: Vec<String> = data3.into_par_iter().collect();
// data3 moved, no longer valid
}The iterator type determines what you can do with elements and collection lifetime.
Performance Implications
use rayon::prelude::*;
fn performance_notes() {
let data: Vec<String> = (0..10000)
.map(|i| format!("string_{}", i))
.collect();
// par_iter: Reference passing
// - No string moves, just references
// - Minimal overhead for the iterator itself
// - But can't take ownership of strings
let total_len: usize = data.par_iter()
.map(|s| s.len())
.sum();
// into_par_iter: Move values
// - Strings are moved into parallel iterator
// - Each thread owns its portion of strings
// - Good when you need to transform/consume
let mut strings = data.clone();
let modified: Vec<String> = strings.into_par_iter()
.map(|mut s| {
s.push_str("_modified");
s
})
.collect();
// par_iter_mut: In-place modification
// - Mutable references, no moves
// - Best for modifying existing collection
let mut strings2 = data.clone();
strings2.par_iter_mut()
.for_each(|s| s.push_str("_modified"));
// Performance order (generally):
// 1. par_iter / par_iter_mut - just references
// 2. into_par_iter - moves values across threads
// (but avoids cloning if you need ownership anyway)
}References are cheapest; moves are necessary when you need ownership.
Working with Different Collection Types
use rayon::prelude::*;
use std::collections::{HashMap, HashSet, BTreeMap};
fn collection_types() {
// Vectors
let vec = vec![1, 2, 3, 4, 5];
let vec_refs: Vec<&i32> = vec.par_iter().collect();
let vec_owned: Vec<i32> = vec.into_par_iter().collect();
// HashMap
let mut map = HashMap::new();
map.insert("a", 1);
map.insert("b", 2);
// par_iter yields (&K, &V)
let keys: Vec<&&str> = map.par_iter().map(|(k, _v)| k).collect();
let values: Vec<&i32> = map.par_iter().map(|(_k, v)| v).collect();
// into_par_iter yields (K, V) - owned key-value pairs
let map = map; // reborrow
let pairs: Vec<(String, i32)> = map.into_par_iter()
.map(|(k, v)| (k.to_string(), v))
.collect();
// HashSet
let set: HashSet<i32> = (0..10).collect();
let set_refs: Vec<&i32> = set.par_iter().collect();
// BTreeMap maintains order
let btree: BTreeMap<i32, String> = (0..5)
.map(|i| (i, format!("value_{}", i)))
.collect();
let btree_values: Vec<&String> = btree.par_iter()
.map(|(_k, v)| v)
.collect();
}All IntoParallelIterator types support both reference and owned iteration.
Reference Types in par_iter
use rayon::prelude::*;
fn reference_types() {
let data = vec![String::from("hello"), String::from("world")];
// par_iter yields &String
data.par_iter().for_each(|s: &String| {
println!("{}", s);
});
// Automatic dereferencing
data.par_iter().for_each(|s| {
// s is &String, can call methods on String
println!("Length: {}", s.len());
});
// Nested references
let nested = vec![&String::from("hello"), &String::from("world")];
nested.par_iter().for_each(|s: &&String| {
println!("{}", s);
});
// Collecting references
let refs: Vec<&String> = data.par_iter().collect();
// refs contains references to strings in data
// Can't clone/move from references without explicit action
// This won't work - can't move out of reference:
// let owned: Vec<String> = data.par_iter().cloned().collect();
// But cloned() helps:
let owned: Vec<String> = data.par_iter().cloned().collect();
// Now we have owned copies
}par_iter yields references; use cloned() if you need owned copies.
Transforming Between Types
use rayon::prelude::*;
fn type_transformations() {
let strings = vec!["1", "2", "3", "4", "5"];
// par_iter with transformation that produces new type
let numbers: Vec<i32> = strings.par_iter()
.map(|s| s.parse::<i32>().unwrap())
.collect();
// strings still exists
// into_par_iter allows complete type transformation
let strings2 = vec!["1", "2", "3"];
let numbers2: Vec<i32> = strings2.into_par_iter()
.map(|s| s.parse().unwrap())
.collect();
// strings2 consumed
// Chain of transformations with into_par_iter
let result: Vec<String> = (0..10)
.into_par_iter()
.map(|x| x * 2)
.map(|x| x.to_string())
.filter(|s| s.len() < 2)
.collect();
// Can transform through multiple types
let final_result: Vec<u64> = result.into_par_iter()
.map(|s| s.parse().unwrap())
.map(|n: u64| n * 100)
.collect();
}into_par_iter enables type transformations that consume the source.
Complex Ownership Patterns
use rayon::prelude::*;
struct Item {
name: String,
values: Vec<i32>,
}
fn complex_patterns() {
let items: Vec<Item> = (0..10)
.map(|i| Item {
name: format!("item_{}", i),
values: vec![i, i * 2, i * 3],
})
.collect();
// par_iter: read from items
let names: Vec<&String> = items.par_iter()
.map(|item| &item.name)
.collect();
// items still valid
// into_par_iter: extract owned data
let owned_names: Vec<String> = items.into_par_iter()
.map(|item| item.name)
.collect();
// items consumed, names extracted
// Pattern: create intermediate, transform, consume
let processed: Vec<String> = (0..5)
.into_par_iter()
.map(|i| format!("{}_{}", i, i * 2))
.collect::<Vec<_>>() // Collect intermediate
.into_par_iter() // Re-iterate owned
.map(|s| s.to_uppercase())
.collect();
}Owned values enable extracting fields and complex transformations.
Practical Example: Data Processing Pipeline
use rayon::prelude::*;
fn data_pipeline() {
// Scenario: Process a large dataset
let data: Vec<Record> = load_records();
// Stage 1: Read-only analysis with par_iter
let valid_count: usize = data.par_iter()
.filter(|r| r.is_valid())
.count();
println!("Valid records: {}", valid_count);
// Stage 2: In-place normalization with par_iter_mut
let mut normalized_data = data;
normalized_data.par_iter_mut()
.for_each(|r| r.normalize());
// Stage 3: Transform and extract with into_par_iter
let processed: Vec<ProcessedRecord> = normalized_data.into_par_iter()
.filter_map(|r| r.process())
.collect();
// Stage 4: Final aggregation on processed data
let summary: Summary = processed.par_iter()
.fold(Summary::default, |mut acc, r| {
acc.merge(r);
acc
})
.reduce(Summary::default, |a, b| a.combine(b));
}
#[derive(Clone)]
struct Record {
id: i32,
value: f64,
}
impl Record {
fn is_valid(&self) -> bool { self.value > 0.0 }
fn normalize(&mut self) { self.value = self.value.min(1.0).max(0.0); }
fn process(self) -> Option<ProcessedRecord> {
if self.value > 0.5 {
Some(ProcessedRecord { id: self.id, score: self.value })
} else {
None
}
}
}
struct ProcessedRecord {
id: i32,
score: f64,
}
#[derive(Default)]
struct Summary {
count: usize,
total: f64,
}
impl Summary {
fn merge(&mut self, r: &ProcessedRecord) {
self.count += 1;
self.total += r.score;
}
fn combine(self, other: Summary) -> Summary {
Summary {
count: self.count + other.count,
total: self.total + other.total,
}
}
}
fn load_records() -> Vec<Record> {
(0..100).map(|i| Record { id: i, value: i as f64 / 100.0 }).collect()
}Choose the iterator type based on the operation's ownership requirements.
Common Patterns and Anti-Patterns
use rayon::prelude::*;
fn patterns() {
let data = vec![String::from("hello"), String::from("world")];
// GOOD: par_iter when you need the collection later
let len_sum: usize = data.par_iter().map(|s| s.len()).sum();
println!("Collection still usable: {:?}", data);
// GOOD: into_par_iter when consuming is fine
let owned: Vec<String> = data.into_par_iter()
.map(|s| s.to_uppercase())
.collect();
// data no longer needed
// ANTI-PATTERN: par_iter followed by clone when you could use into_par_iter
let data2 = vec![String::from("a"), String::from("b")];
let cloned: Vec<String> = data2.par_iter()
.cloned() // Unnecessary clone
.map(|s| s.to_uppercase())
.collect();
// data2 not used afterward, but cloned
// BETTER: use into_par_iter
let data3 = vec![String::from("a"), String::from("b")];
let owned: Vec<String> = data3.into_par_iter()
.map(|s| s.to_uppercase())
.collect();
// GOOD: par_iter_mut for in-place modification
let mut data4 = vec![1, 2, 3, 4, 5];
data4.par_iter_mut().for_each(|x| *x *= 2);
// Efficient in-place modification
// ANTI-PATTERN: Collecting to modify
let data5 = vec![1, 2, 3, 4, 5];
let modified: Vec<i32> = data5.into_par_iter()
.map(|x| x * 2)
.collect();
// If you needed data5 to still exist as a Vec, this is wrong
}Match the iterator type to ownership needs, not convenience.
Summary Table
fn summary() {
// | Method | Takes | Yields | Collection After |
// |----------------|------------|--------|------------------|
// | par_iter | &self | &T | Preserved (read) |
// | par_iter_mut | &mut self | &mut T | Preserved (mut) |
// | into_par_iter | self | T | Consumed |
// | Use Case | Best Method |
// |------------------------------|-------------------|
// | Read-only operations | par_iter |
// | Need collection afterward | par_iter |
// | In-place modification | par_iter_mut |
// | Transform to new type | into_par_iter |
// | Collection is temporary | into_par_iter |
// | Extract owned values | into_par_iter |
// | Operation | Ownership Needed |
// |------------------------------|------------------|
// | Read field | Reference (&T) |
// | Mutate field in place | Mut ref (&mut T) |
// | Transform to new type | Owned (T) |
// | Move out of collection | Owned (T) |
// | Call consuming methods | Owned (T) |
}Synthesis
Quick reference:
use rayon::prelude::*;
let data = vec![String::from("hello"), String::from("world")];
// par_iter: borrow, preserve collection
let refs: Vec<&String> = data.par_iter().collect();
// data still usable after
// par_iter_mut: mutably borrow, preserve collection
let mut data2 = data.clone();
data2.par_iter_mut().for_each(|s| s.push('!'));
// data2 modified in place, still usable
// into_par_iter: consume collection, get owned values
let owned: Vec<String> = data.into_par_iter().collect();
// data is moved, no longer usableKey insight: The choice between par_iter, par_iter_mut, and into_par_iter is fundamentally about ownership semantics. par_iter takes &self, yielding &T references that allow parallel read operations while preserving the collection for subsequent use. par_iter_mut takes &mut self, yielding &mut T references that allow parallel in-place mutation while still preserving the collection. into_par_iter takes self by value, consuming the collection and yielding owned T values that can be transformed, moved into new structures, or have consuming methods called on them. The reference-yielding variants (par_iter, par_iter_mut) are more efficient for their respective use cases because they avoid moving dataâbut they're also more restrictive. Owned values from into_par_iter enable operations that require ownership: calling .into_bytes() on String, returning values that outlive the iteration, or completely transforming types. Use par_iter when you need multiple passes or the collection must outlive the operation; use par_iter_mut when you want to modify elements without allocating a new collection; use into_par_iter when the collection is temporary or you need to transform ownership. The performance difference between references and moves is usually negligible compared to the parallel computation itselfâchoose based on what your algorithm needs, not micro-optimization.
