What is the purpose of rayon::iter::IntoParallelIterator::into_par_iter for converting collections into parallel iterators?

into_par_iter consumes a collection and transforms it into a parallel iterator, enabling Rayon to distribute the work across multiple threads automatically. This is the entry point for parallel iteration—you call into_par_iter() on any collection that implements IntoParallelIterator, and Rayon handles splitting the data, distributing chunks to threads, and collecting results. Unlike par_iter() which borrows the collection, into_par_iter() takes ownership, allowing the parallel iterator to consume and potentially rearrange or drop elements without needing to return them to the original collection.

Basic Parallel Iteration

use rayon::prelude::*;
 
fn main() {
    let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // into_par_iter consumes the vector
    let sum: i32 = data.into_par_iter().sum();
    
    println!("Sum: {}", sum);  // 55
    
    // data is no longer available - it was consumed
    // println!("{:?}", data);  // Error: value borrowed after move
}

into_par_iter takes ownership and enables parallel processing.

Ownership vs Borrowing

use rayon::prelude::*;
 
fn main() {
    // into_par_iter: Takes ownership
    let data = vec![1, 2, 3, 4, 5];
    let result: Vec<i32> = data.into_par_iter()
        .map(|x| x * 2)
        .collect();
    println!("Result: {:?}", result);
    // data is consumed, cannot be used again
    
    // par_iter: Borrows the collection
    let data = vec![1, 2, 3, 4, 5];
    let result: Vec<i32> = data.par_iter()
        .map(|x| x * 2)
        .collect();
    println!("Result: {:?}", result);
    println!("Original still available: {:?}", data);  // Still owned
    
    // par_iter_mut: Borrows mutably
    let mut data = vec![1, 2, 3, 4, 5];
    data.par_iter_mut().for_each(|x| *x *= 2);
    println!("Modified in place: {:?}", data);
}

Three ways to iterate in parallel: consume, borrow, or borrow mutably.

Transformations with into_par_iter

use rayon::prelude::*;
 
fn main() {
    let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    // Chain transformations, all executed in parallel
    let result: Vec<i32> = data.into_par_iter()
        .filter(|x| x % 2 == 0)
        .map(|x| x * x)
        .take(3)
        .collect();
    
    println!("Filtered, squared, limited: {:?}", result);  // [4, 16, 36]
}

Complex transformations chain naturally with parallel iterators.

Collecting Results

use rayon::prelude::*;
use std::collections::HashMap;
 
fn main() {
    let words = vec!["apple", "banana", "cherry", "date", "elderberry"];
    
    // Collect into different types
    let lengths: HashMap<String, usize> = words.into_par_iter()
        .map(|word| (word.to_string(), word.len()))
        .collect();
    
    println!("Word lengths: {:?}", lengths);
    
    // Collect into a BTreeMap for sorted keys
    let sorted: std::collections::BTreeMap<String, usize> = lengths.into_iter().collect();
    println!("Sorted: {:?}", sorted);
    
    // Reduce to a single value
    let data = (1..=100).collect::<Vec<_>>();
    let sum: i32 = data.into_par_iter().sum();
    println!("Sum of 1-100: {}", sum);  // 5050
}

Results can be collected into various container types.

Performance Characteristics

use rayon::prelude::*;
 
fn main() {
    let data: Vec<i32> = (1..=1_000_000).collect();
    
    // Serial iteration
    let start = std::time::Instant::now();
    let sum_serial: i32 = data.iter().map(|x| x * 2).sum();
    let serial_time = start.elapsed();
    
    // Parallel iteration with into_par_iter
    let start = std::time::Instant::now();
    let sum_parallel: i32 = data.clone().into_par_iter().map(|x| x * 2).sum();
    let parallel_time = start.elapsed();
    
    println!("Serial: {:?}, Parallel: {:?}", serial_time, parallel_time);
    println!("Serial sum: {}, Parallel sum: {}", sum_serial, sum_parallel);
    
    // Note: Parallel overhead exists for small collections
    // into_par_iter shines with:
    // - Large collections
    // - Expensive operations per element
    // - CPU-intensive work
}

Parallel iteration has overhead; benefits scale with data size and operation cost.

Types Implementing IntoParallelIterator

use rayon::prelude::*;
use std::collections::{HashMap, HashSet, BTreeMap};
 
fn main() {
    // Vec
    let vec_result: Vec<i32> = vec![1, 2, 3].into_par_iter().collect();
    
    // Range
    let range_result: Vec<i32> = (1..=10).into_par_iter().collect();
    
    // HashSet
    let set: HashSet<i32> = vec![1, 2, 3].into_iter().collect();
    let set_result: Vec<i32> = set.into_par_iter().collect();
    
    // HashMap
    let map: HashMap<i32, &str> = vec![(1, "a"), (2, "b")].into_iter().collect();
    let map_result: Vec<(i32, String)> = map.into_par_iter()
        .map(|(k, v)| (k * 2, v.to_uppercase()))
        .collect();
    
    // BTreeMap
    let btree: BTreeMap<i32, &str> = vec![(3, "c"), (1, "a"), (2, "b")].into_iter().collect();
    let btree_result: Vec<i32> = btree.into_par_iter().map(|(k, _)| k).collect();
    
    println!("All types support into_par_iter");
}

Many standard collections implement IntoParallelIterator.

Parallel Map and Filter

use rayon::prelude::*;
 
fn main() {
    let data: Vec<i32> = (1..=100).collect();
    
    // Expensive computation in parallel
    let result: Vec<String> = data.into_par_iter()
        .filter(|x| x % 3 == 0)
        .map(|x| {
            // Simulate expensive work
            (0..100).for_each(|_| {
                let _ = x.pow(2);
            });
            format!("Number: {}", x)
        })
        .collect();
    
    println!("Processed {} items", result.len());
    
    // Parallel iteration with indices
    let indexed: Vec<(usize, i32)> = (10..=20).into_par_iter().enumerate().collect();
    println!("With indices: {:?}", indexed);
}

Expensive operations benefit most from parallelization.

Reduce and Fold Operations

use rayon::prelude::*;
 
fn main() {
    let data: Vec<i32> = (1..=1000).collect();
    
    // reduce: Takes closure with two elements
    let max: i32 = data.clone().into_par_iter().reduce(|| i32::MIN, |a, b| a.max(b));
    println!("Max: {}", max);
    
    // reduce_with: Returns Option, identity not needed
    let min: Option<i32> = data.clone().into_par_iter().reduce_with(|a, b| a.min(b));
    println!("Min: {:?}", min);
    
    // fold: Accumulates with state, then combines
    let sum_of_squares: i32 = data.clone().into_par_iter()
        .fold(
            || 0,           // Identity for each thread
            |acc, x| acc + x * x,  // Combine element with accumulator
            |a, b| a + b    // Combine thread results
        );
    println!("Sum of squares: {}", sum_of_squares);
    
    // Alternative: map and sum
    let sum_squares_alt: i32 = data.into_par_iter().map(|x| x * x).sum();
    assert_eq!(sum_of_squares, sum_squares_alt);
}

Reduce and fold aggregate results from parallel threads.

Working with Custom Types

use rayon::prelude::*;
 
#[derive(Debug, Clone)]
struct User {
    id: u32,
    name: String,
    active: bool,
}
 
fn main() {
    let users: Vec<User> = (1..=100)
        .map(|id| User {
            id,
            name: format!("User{}", id),
            active: id % 2 == 0,
        })
        .collect();
    
    // Process custom types in parallel
    let active_names: Vec<String> = users.into_par_iter()
        .filter(|u| u.active)
        .map(|u| u.name)
        .collect();
    
    println!("Active users: {:?}", active_names.len());
    
    // Complex transformation
    let users: Vec<User> = (1..=50)
        .map(|id| User {
            id,
            name: format!("User{}", id),
            active: id % 3 == 0,
        })
        .collect();
    
    let processed: Vec<String> = users.into_par_iter()
        .filter(|u| u.active)
        .map(|u| format!("{}: {}", u.id, u.name))
        .collect();
    
    println!("Processed {} active users", processed.len());
}

Custom types work naturally with parallel iterators.

Thread Safety Requirements

use rayon::prelude::*;
use std::sync::Mutex;
 
fn main() {
    // Rayon requires Send + Sync for parallel execution
    let data: Vec<i32> = (1..=100).collect();
    
    // This works: i32 is Send + Sync
    let sum: i32 = data.into_par_iter().sum();
    println!("Sum: {}", sum);
    
    // Using Mutex for shared state (avoid this pattern when possible)
    let counter = Mutex::new(0);
    (1..=100).into_par_iter().for_each(|_| {
        let mut guard = counter.lock().unwrap();
        *guard += 1;
    });
    println!("Counter: {}", *counter.lock().unwrap());
    
    // Better: Use reduce/fold instead of shared state
    let count: i32 = (1..=100).into_par_iter().map(|_| 1).sum();
    println!("Count: {}", count);
}

Types must be Send + Sync for parallel iteration.

Comparing Iterator Methods

use rayon::prelude::*;
 
fn main() {
    // into_par_iter: Consumes collection, returns owned items
    let v = vec![1, 2, 3];
    let result: Vec<i32> = v.into_par_iter().collect();
    // v is gone
    
    // par_iter: Borrows collection, returns references
    let v = vec![1, 2, 3];
    let result: Vec<&i32> = v.par_iter().collect();
    // v is still available
    println!("Still have v: {:?}", v);
    
    // par_iter_mut: Borrows mutably, returns mutable references
    let mut v = vec![1, 2, 3];
    v.par_iter_mut().for_each(|x| *x *= 2);
    println!("Modified: {:?}", v);
    
    // Choose based on what you need:
    // - into_par_iter: When you're done with the collection
    // - par_iter: When you need to keep using the collection
    // - par_iter_mut: When you want to modify in place
}

Choose the method based on ownership needs.

Practical Example: Data Processing

use rayon::prelude::*;
 
struct DataPoint {
    x: f64,
    y: f64,
}
 
fn main() {
    let data: Vec<DataPoint> = (0..10_000)
        .map(|i| DataPoint {
            x: i as f64 * 0.1,
            y: (i as f64 * 0.1).sin(),
        })
        .collect();
    
    // Process in parallel
    let processed: Vec<f64> = data.into_par_iter()
        .map(|p| p.x * p.y)
        .collect();
    
    println!("Processed {} data points", processed.len());
    
    // Statistical calculations
    let data: Vec<f64> = (0..100_000).map(|i| i as f64 * 0.001).collect();
    
    let (sum, count) = data.into_par_iter()
        .fold(
            || (0.0, 0),
            |(sum, count), x| (sum + x, count + 1),
            |(s1, c1), (s2, c2)| (s1 + s2, c1 + c2),
        );
    
    let mean = sum / count as f64;
    println!("Mean: {}", mean);
}

Parallel processing scales well for large datasets.

Flat Map and Flatten

use rayon::prelude::*;
 
fn main() {
    // Flatten nested structures
    let nested: Vec<Vec<i32>> = vec![
        vec![1, 2, 3],
        vec![4, 5, 6],
        vec![7, 8, 9],
    ];
    
    let flattened: Vec<i32> = nested.into_par_iter()
        .flatten()
        .collect();
    
    println!("Flattened: {:?}", flattened);
    
    // flat_map for transformation + flattening
    let data: Vec<&str> = vec!["a,b", "c,d", "e,f"];
    
    let result: Vec<&str> = data.into_par_iter()
        .flat_map(|s| s.split(','))
        .collect();
    
    println!("Flat mapped: {:?}", result);  // ["a", "b", "c", "d", "e", "f"]
}

Nested structures flatten naturally in parallel.

Partition and Grouping

use rayon::prelude::*;
use std::collections::HashMap;
 
fn main() {
    let data: Vec<i32> = (1..=20).collect();
    
    // Partition into two groups
    let (evens, odds): (Vec<i32>, Vec<i32>) = data.into_par_iter()
        .partition(|x| x % 2 == 0);
    
    println!("Evens: {:?}", evens);
    println!("Odds: {:?}", odds);
    
    // Group by key (requires sequential processing after parallel)
    let words = vec!["apple", "banana", "cherry", "date", "elderberry"];
    let grouped: HashMap<char, Vec<String>> = words.into_par_iter()
        .map(|s| (s.chars().next().unwrap(), s.to_string()))
        .collect::<Vec<_>>()
        .into_iter()
        .fold(HashMap::new(), |mut acc, (k, v)| {
            acc.entry(k).or_insert_with(Vec::new).push(v);
            acc
        });
    
    println!("Grouped by first letter: {:?}", grouped);
}

Partition divides data; grouping may require sequential reduction.

Error Handling with try_reduce

use rayon::prelude::*;
 
fn main() {
    let data: Vec<i32> = (1..=100).collect();
    
    // Fallible parallel reduction
    let result: Result<i32, &str> = data.clone().into_par_iter()
        .try_reduce(|| 0, |a, b| {
            if a + b > 1000 {
                Err("Sum too large")
            } else {
                Ok(a + b)
            }
        });
    
    println!("Result: {:?}", result);
    
    // try_reduce_with returns Option
    let result: Option<Result<i32, &str>> = data.into_par_iter()
        .try_reduce_with(|a, b| {
            if a + b > 1000 {
                Err("Sum too large")
            } else {
                Ok(a + b)
            }
        });
    
    println!("Result: {:?}", result);
}

Fallible operations use try_reduce and try_reduce_with.

Synthesis

Quick reference:

use rayon::prelude::*;
 
// Basic usage
let sum: i32 = vec.into_par_iter().sum();
 
// Three ways to parallel iterate:
// 1. into_par_iter() - Takes ownership, returns owned items
// 2. par_iter() - Borrows, returns references  
// 3. par_iter_mut() - Mutably borrows, returns mutable references
 
// When to use into_par_iter:
// - Collection won't be needed after iteration
// - Need owned values (not references)
// - Transforming and collecting into new structure
// - Working with Copy types where clone cost matters
 
// Common operations:
data.into_par_iter()
    .filter(|x| *x > 0)      // Filter
    .map(|x| x * 2)          // Transform
    .take(100)               // Limit
    .collect()               // Collect results
    ;
 
data.into_par_iter().reduce(|| 0, |a, b| a + b);  // Reduce
data.into_par_iter().sum();                       // Sum
data.into_par_iter().for_each(|x| println!("{}", x));  // Side effects
 
// Types implementing IntoParallelIterator:
// - Vec, VecDeque, LinkedList
// - HashSet, BTreeSet
// - HashMap, BTreeMap
// - Range, RangeInclusive
// - Arrays, slices
// - Any IntoIterator where Item: Send

Key insight: into_par_iter is Rayon's bridge between owned collections and parallel execution. It takes ownership of the collection, which enables the parallel iterator to freely distribute elements across threads without lifetime concerns—each thread gets its own chunk of owned data. This is distinct from par_iter() which borrows and par_iter_mut() which mutably borbs, both of which require the collection to live as long as the parallel operation. Use into_par_iter when you're transforming a collection into something new and don't need the original anymore; use par_iter when you need to keep the collection; use par_iter_mut when you want to modify in place. The ownership transfer is what enables clean, parallel-safe code without fighting the borrow checker.