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 valid

Key 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.