How does rayon::iter::IntoParallelIterator::into_par_iter differ from par_iter for owned vs borrowed collections?
into_par_iter consumes the collection and produces owned items in parallel, enabling transformations that require ownership (like map to different types), while par_iter borrows the collection and yields references, restricting operations to those that work with borrowed data. The fundamental distinction mirrors the difference between into_iter() and iter() on sequential iteratorsāinto_par_iter takes ownership of elements, par_iter only borrows them.
Borrowed Parallel Iteration
use rayon::prelude::*;
fn borrowed_iteration() {
let data = vec![1, 2, 3, 4, 5];
// par_iter() borrows the vector
// Yields &i32 references
let sum: i32 = data.par_iter().sum();
println!("Sum: {}", sum); // 15
// data is still accessible after par_iter
println!("Still have: {:?}", data); // [1, 2, 3, 4, 5]
// par_iter yields references
data.par_iter().for_each(|x| {
// x is &i32
println!("Reference to: {}", x);
});
// Can only do reference-compatible operations
// data.par_iter().map(|x| *x + 1).collect::<Vec<_>>(); // Can map, but...
// Cannot consume items for owned operations
}par_iter borrows the collection, producing parallel iterators over references.
Owned Parallel Iteration
use rayon::prelude::*;
fn owned_iteration() {
let data = vec![1, 2, 3, 4, 5];
// into_par_iter() consumes the vector
// Yields i32 (owned)
let sum: i32 = data.into_par_iter().sum();
println!("Sum: {}", sum); // 15
// data is MOVED - cannot access afterward
// println!("{:?}", data); // Compile error: value borrowed after move
// into_par_iter yields owned values
let data = vec![String::from("hello"), String::from("world")];
data.into_par_iter().for_each(|s| {
// s is String (owned)
// Can take ownership, modify, consume
let _owned = s; // Ownership transferred
});
}into_par_iter consumes the collection, producing owned items.
Element Ownership Differences
use rayon::prelude::*;
fn element_ownership() {
let strings = vec![
String::from("hello"),
String::from("world"),
String::from("rayon"),
];
// par_iter: elements are references
strings.par_iter().for_each(|s| {
// s is &String
println!("Borrowed: {}", s);
});
// into_par_iter: elements are owned
strings.into_par_iter().for_each(|s| {
// s is String (owned)
let owned: String = s; // Can take ownership
// owned is consumed here
});
// strings is moved, can't use anymore
}The element type differs: par_iter yields references, into_par_iter yields owned values.
Transformation Type Changes
use rayon::prelude::*;
fn type_transformations() {
let numbers = vec![1, 2, 3, 4, 5];
// par_iter: Can only produce references or copies
let doubled: Vec<i32> = numbers.par_iter()
.map(|&x| x * 2) // Copy i32
.collect();
// numbers still accessible
println!("Original: {:?}", numbers);
// into_par_iter: Can transform to different types
let strings: Vec<String> = vec![1, 2, 3].into_par_iter()
.map(|x| format!("num_{}", x)) // Consume i32, produce String
.collect();
// For owned types like String, into_par_iter is necessary
let owned_strings = vec![String::from("a"), String::from("b")];
// This works with into_par_iter
let uppercase: Vec<String> = owned_strings.into_par_iter()
.map(|s| s.to_uppercase()) // Consume String
.collect();
// With par_iter, would need to clone:
// let uppercase: Vec<String> = strings.par_iter()
// .map(|s| s.to_uppercase()) // Creates new String from reference
// .collect();
}into_par_iter enables type transformations without cloning; par_iter may require cloning.
Collection Consumption Patterns
use rayon::prelude::*;
fn consumption_patterns() {
// par_iter: Collection survives
let data = vec![1, 2, 3, 4, 5];
let sum: i32 = data.par_iter().sum();
let product: i32 = data.par_iter().product(); // Reuse collection
println!("Sum: {}, Product: {}", sum, product);
// into_par_iter: Collection consumed
let data = vec![1, 2, 3, 4, 5];
let sum: i32 = data.into_par_iter().sum();
// data is moved - cannot reuse
// let product = data.into_par_iter().product(); // Error: use of moved value
// Pattern: Use par_iter when collection needed afterward
// Pattern: Use into_par_iter when collection not needed
// Common case: Final transformation
let data = vec![1, 2, 3, 4, 5];
let processed: Vec<String> = data.into_par_iter()
.map(|n| format!("value_{}", n))
.collect();
// No need for data afterward
}Use par_iter when reusing the collection; into_par_iter when it's the final use.
Clone Requirements
use rayon::prelude::*;
fn clone_requirements() {
let strings = vec![
String::from("hello"),
String::from("world"),
];
// With par_iter, need to clone for owned values
let clones: Vec<String> = strings.par_iter()
.cloned() // Clone each reference
.collect();
// strings still valid
println!("Original: {:?}", strings);
// With into_par_iter, no cloning needed
let owned: Vec<String> = strings.into_par_iter()
.collect(); // Already owned
// No Clone requirement for into_par_iter usage
struct NoClone(i32);
// NoClone doesn't implement Clone
let items: Vec<NoClone> = vec![NoClone(1), NoClone(2), NoClone(3)];
// into_par_iter works fine
let sum: i32 = items.into_par_iter()
.map(|NoClone(n)| n)
.sum();
// par_iter would give &NoClone, can't clone
}into_par_iter avoids clone requirements for owned operations.
Reference Types in Parallel
use rayon::prelude::*;
fn reference_types() {
let data = vec![vec![1, 2], vec![3, 4], vec![5, 6]];
// par_iter yields &Vec<i32>
let lengths: Vec<usize> = data.par_iter()
.map(|v| v.len()) // &Vec<i32> -> usize
.collect();
// data is still accessible
println!("Original: {:?}", data);
// into_par_iter yields Vec<i32>
let flattened: Vec<i32> = data.into_par_iter()
.flat_map(|v| v) // Vec<i32> into iterator
.collect();
// into_par_iter allows consuming inner collections
// For nested structures, ownership matters
let nested = vec![
vec![String::from("a"), String::from("b")],
vec![String::from("c")],
];
// par_iter: references to inner vectors
let inner_refs: Vec<&[String]> = nested.par_iter()
.map(|v| v.as_slice())
.collect();
// into_par_iter: owned inner vectors
let all_strings: Vec<String> = nested.into_par_iter()
.flatten() // Flatten owned Vec<String> into Strings
.collect();
}For nested structures, into_par_iter allows consuming inner collections.
Mutable Parallel Iteration
use rayon::prelude::*;
fn mutable_iteration() {
let mut data = vec![1, 2, 3, 4, 5];
// par_iter_mut: Mutable references
data.par_iter_mut().for_each(|x| {
*x *= 2; // Modify in place
});
println!("Doubled: {:?}", data); // [2, 4, 6, 8, 10]
// data is still accessible (modified in place)
// into_par_iter would consume data
// No equivalent "into_par_iter_mut" - that doesn't make sense
// par_iter_mut is for in-place modifications
// into_par_iter is for consuming and transforming
}par_iter_mut provides mutable references for in-place modification.
Complex Transformations
use rayon::prelude::*;
fn complex_transformations() {
struct Item {
name: String,
value: i32,
}
let items = vec![
Item { name: String::from("a"), value: 1 },
Item { name: String::from("b"), value: 2 },
Item { name: String::from("c"), value: 3 },
];
// into_par_iter: Can extract owned String
let names: Vec<String> = items.into_par_iter()
.map(|item| item.name) // Take ownership of name
.collect();
// par_iter: Must clone or copy
let items = vec![
Item { name: String::from("a"), value: 1 },
Item { name: String::from("b"), value: 2 },
];
let names: Vec<String> = items.par_iter()
.map(|item| item.name.clone()) // Must clone
.collect();
// items still accessible
// For types without Clone, into_par_iter is essential
struct UniqueId(u64);
let ids: Vec<UniqueId> = vec![UniqueId(1), UniqueId(2), UniqueId(3)];
// Can't clone UniqueId
// Must use into_par_iter to extract owned values
let raw_ids: Vec<u64> = ids.into_par_iter()
.map(|UniqueId(n)| n)
.collect();
}into_par_iter is necessary when you need ownership of non-Clone types.
Memory and Performance
use rayon::prelude::*;
fn memory_performance() {
let large_data: Vec<String> = (0..10000)
.map(|i| format!("string_{}", i))
.collect();
// par_iter: References, no ownership transfer
// - Lower memory overhead (just references)
// - Original collection remains intact
// - May need to clone for owned operations
let total_len: usize = large_data.par_iter()
.map(|s| s.len()) // Just read, no allocation
.sum();
// into_par_iter: Ownership transferred
// - Original collection deallocated during iteration
// - No cloning needed
// - Can transform in place
let lengths: Vec<usize> = large_data.into_par_iter()
.map(|s| s.len()) // String deallocated after extracting length
.collect();
// large_data is consumed - memory freed during iteration
// Performance consideration:
// - par_iter: Keep original + references
// - into_par_iter: Original freed as elements consumed
}into_par_iter can reduce memory usage by freeing elements as they're processed.
Common Patterns
use rayon::prelude::*;
fn common_patterns() {
// Pattern 1: Read-only analysis (par_iter)
let data = vec![1, 2, 3, 4, 5];
let max = data.par_iter().max();
let min = data.par_iter().min();
// Collection reused, still valid
// Pattern 2: Final transformation (into_par_iter)
let data = vec![1, 2, 3, 4, 5];
let strings: Vec<String> = data.into_par_iter()
.map(|n| format!("num_{}", n))
.collect();
// Collection consumed, result produced
// Pattern 3: In-place modification (par_iter_mut)
let mut data = vec![1, 2, 3, 4, 5];
data.par_iter_mut().for_each(|x| *x *= 2);
// Collection modified in place
// Pattern 4: Owned transformation (into_par_iter)
let files: Vec<std::path::PathBuf> = vec![];
let contents: Vec<String> = files.into_par_iter()
.map(|path| std::fs::read_to_string(path).unwrap_or_default())
.collect();
// Files consumed, contents produced
// Pattern 5: Filter with ownership (into_par_iter)
let items = vec![String::from("keep"), String::from("remove"), String::from("keep")];
let filtered: Vec<String> = items.into_par_iter()
.filter(|s| s.contains("keep"))
.collect();
}Choose based on whether you need the collection afterward and whether you need owned elements.
Indexed Parallel Iterator
use rayon::prelude::*;
fn indexed_iteration() {
let data = vec!['a', 'b', 'c', 'd', 'e'];
// par_iter: enumerate gives (&usize, &char)
data.par_iter().enumerate().for_each(|(i, &c)| {
println!("{} at index {}", c, i);
});
// into_par_iter: enumerate gives (usize, char)
data.into_par_iter().enumerate().for_each(|(i, c)| {
// c is owned char
println!("{} at index {}", c, i);
});
// Both support indexed operations
// Difference is ownership of elements
}Both support enumerate(); the difference is element ownership.
Type Signatures
use rayon::prelude::*;
fn type_signatures() {
// par_iter() signature on Vec<T>:
// fn par_iter(&self) -> Iter<'_, T>
// where Iter<'_, T>: ParallelIterator<Item = &T>
// into_par_iter() signature on Vec<T>:
// fn into_par_iter(self) -> IntoIter<T>
// where IntoIter<T>: ParallelIterator<Item = T>
// par_iter_mut() signature on Vec<T>:
// fn par_iter_mut(&mut self) -> IterMut<'_, T>
// where IterMut<'_, T>: ParallelIterator<Item = &mut T>
// The Item type differs:
// par_iter: Item = &T
// into_par_iter: Item = T
// par_iter_mut: Item = &mut T
}The key difference is the Item type: references vs owned vs mutable references.
Converting Between Iterator Types
use rayon::prelude::*;
fn converting_iterators() {
let data = vec![1, 2, 3, 4, 5];
// Can convert owned to references (with Clone)
let refs: Vec<&i32> = data.par_iter().collect();
// Can get owned from references (with Clone)
let owned: Vec<i32> = data.par_iter().cloned().collect();
// into_par_iter is more efficient when you have ownership
let owned_direct: Vec<i32> = data.into_par_iter().collect();
// But sometimes you need both:
let data = vec![1, 2, 3, 4, 5];
// First, read operations
let sum: i32 = data.par_iter().sum();
// Then, owned transformation
let transformed: Vec<String> = data.into_par_iter()
.map(|n| format!("val_{}", n))
.collect();
// Or clone upfront:
let transformed: Vec<String> = data.par_iter()
.cloned()
.map(|n| format!("val_{}", n))
.collect();
// data still valid but cloned unnecessarily
}Choose based on whether you need the original collection after the operation.
Practical Example: File Processing
use rayon::prelude::*;
use std::path::PathBuf;
fn file_processing_example() -> std::io::Result<()> {
let files: Vec<PathBuf> = vec![
PathBuf::from("file1.txt"),
PathBuf::from("file2.txt"),
PathBuf::from("file3.txt"),
];
// into_par_iter: Consume file paths, produce contents
// Each file path is consumed, no cloning needed
let contents: Vec<String> = files.into_par_iter()
.map(|path| {
std::fs::read_to_string(&path).unwrap_or_default()
})
.collect();
// files is consumed - can't reuse
// But we have all contents
// Alternative with par_iter (if files needed):
let files = vec![
PathBuf::from("file1.txt"),
PathBuf::from("file2.txt"),
];
let contents: Vec<String> = files.par_iter()
.map(|path| {
std::fs::read_to_string(path).unwrap_or_default()
})
.collect();
// files still valid - paths borrowed, not consumed
Ok(())
}Use into_par_iter when file paths can be consumed; par_iter when you need to keep them.
Synthesis
Quick comparison:
| Aspect | par_iter |
into_par_iter |
par_iter_mut |
|---|---|---|---|
| Ownership | Borrowed | Consumed | Mutably borrowed |
| Item type | &T |
T |
&mut T |
| Collection after | Still valid | Moved | Still valid (modified) |
| Clone needed | For owned values | Never | Never |
| Use case | Read operations, reuse | Final transformation | In-place modification |
Decision guide:
use rayon::prelude::*;
// Use par_iter when:
// - You need the collection afterward
// - Only reading elements
// - Don't need ownership of elements
let sum: i32 = data.par_iter().sum();
// data still available
// Use into_par_iter when:
// - You don't need the collection afterward
// - Transforming to different types
// - Working with non-Clone types
// - Want to avoid cloning
let strings: Vec<String> = numbers.into_par_iter()
.map(|n| format!("num_{}", n))
.collect();
// numbers consumed, no clone overhead
// Use par_iter_mut when:
// - Modifying elements in place
// - Don't need to change collection size
data.par_iter_mut().for_each(|x| *x *= 2);
// data modified in place, still validKey insight: The par_iter vs into_par_iter distinction mirrors Rust's ownership model applied to parallel iteration. Just as iter() yields references and into_iter() yields owned values in sequential iteration, par_iter() borrows elements while into_par_iter() consumes them. The critical consequence is that into_par_iter enables transformations that require ownership without forcing a clone() callāthis matters for expensive-to-clone types like String or Vec, and is essential for types that don't implement Clone at all. Meanwhile, par_iter keeps the collection alive for subsequent operations, avoiding reallocation if you need to iterate multiple times. The choice is fundamentally about whether you need the collection after the parallel operation and whether your transformation requires owned elements.
