Что такое Rayon и зачем он нужен?
Rayon — это мощная библиотека для языка программирования Rust, которая предоставляет простой и безопасный способ распараллеливания вычислений. Её главная особенность — концепция параллельных итераторов. Вместо того чтобы вручную управлять потоками, мьютексами и каналами, вы просто заменяете последовательный итератор (`.iter()`) на параллельный (`.par_iter()`), и Rayon автоматически распределяет работу по всем доступным ядрам процессора.
Зачем нужна эта библиотека? В современном мире многоядерные процессоры стали стандартом. Однако написание многопоточного кода вручную — это сложная задача, чреватая гонками данных, взаимными блокировками и другими трудноуловимыми ошибками. Rayon берёт на себя все эти сложности, предоставляя:
- Автоматическое распределение работы: Библиотека сама решает, как разбить данные на части и распределить их между потоками.
- Безопасность на уровне типов: Благодаря системе владения и заимствования Rust, Rayon гарантирует отсутствие гонок данных на этапе компиляции.
- Work-stealing (кража работы): Если один поток завершил свою задачу раньше других, он "крадёт" работу у занятых потоков, обеспечивая равномерную загрузку всех ядер.
- Простота использования: API Rayon интуитивно понятен для любого разработчика, знакомого с итераторами Rust.
Rayon идеально подходит для задач, где нужно обработать большие объёмы данных: обработка изображений, симуляции, численные расчёты, парсинг файлов и многое другое. Если ваша задача легко разбивается на независимые подзадачи (data parallelism), Rayon — ваш выбор.
Установка
Rayon устанавливается стандартным способом для Rust — через менеджер пакетов Cargo. Добавьте следующую строку в файл Cargo.toml вашего проекта:
[dependencies]
rayon = "1.10"
После этого выполните команду cargo build, и Cargo автоматически загрузит и скомпилирует библиотеку. Rayon — это "чисто" Rust библиотека, поэтому никаких дополнительных зависимостей или системных требований не требуется. Она работает на Windows, macOS и Linux.
Быстрый старт
Давайте начнём с классического примера: суммирование большого массива чисел. Сначала сделаем это последовательно, а затем — параллельно с помощью Rayon.
// Подключаем библиотеку
use rayon::prelude::*;
fn main() {
// Создаём большой вектор чисел
let numbers: Vec<u64> = (1..=10_000_000).collect();
// --- Последовательное суммирование ---
let sequential_start = std::time::Instant::now();
let sum_sequential: u64 = numbers.iter().sum();
let sequential_duration = sequential_start.elapsed();
println!("Последовательная сумма: {} (время: {:?})", sum_sequential, sequential_duration);
// --- Параллельное суммирование с помощью Rayon ---
let parallel_start = std::time::Instant::now();
let sum_parallel: u64 = numbers.par_iter().sum(); // Заменили .iter() на .par_iter()
let parallel_duration = parallel_start.elapsed();
println!("Параллельная сумма: {} (время: {:?})", sum_parallel, parallel_duration);
// Сравнение ускорения
println!("Ускорение: {:.2}x", sequential_duration.as_secs_f64() / parallel_duration.as_secs_f64());
}
В этом примере мы сделали всего одно изменение: заменили .iter() на .par_iter(). Rayon автоматически разбил вектор на части, распределил их по потокам, вычислил частичные суммы, а затем объединил результаты. На многоядерном процессоре вы увидите значительное ускорение.
Основные методы/функции
1. par_iter()
Что делает: Создаёт параллельный итератор из ссылок на элементы коллекции. Это самый распространённый способ начать работу с Rayon. Он заимствует данные, поэтому исходная коллекция остаётся доступной после итерации.
Параметры: Принимает ссылку на коллекцию, реализующую трейт IntoParallelRefIterator (например, Vec, HashMap, slice).
Пример:
use rayon::prelude::*;
fn main() {
let vec = vec![1, 2, 3, 4, 5];
// Параллельно умножаем каждый элемент на 2
let doubled: Vec<i32> = vec.par_iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // [2, 4, 6, 8, 10]
// Исходный вектор всё ещё доступен
println!("{:?}", vec); // [1, 2, 3, 4, 5]
}
2. par_iter_mut()
Что делает: Создаёт параллельный итератор, который даёт изменяемые ссылки на элементы коллекции. Позволяет модифицировать элементы на месте.
Параметры: Принимает изменяемую ссылку на коллекцию, реализующую трейт IntoParallelRefMutIterator.
Пример:
use rayon::prelude::*;
fn main() {
let mut vec = vec![1, 2, 3, 4, 5];
// Параллельно увеличиваем каждый элемент на 1
vec.par_iter_mut().for_each(|x| *x += 1);
println!("{:?}", vec); // [2, 3, 4, 5, 6]
}
3. into_par_iter()
Что делает: Создаёт параллельный итератор, который потребляет (перемещает) исходную коллекцию. После вызова этого метода исходная коллекция становится недоступной. Это полезно, когда вам больше не нужна исходная коллекция, и вы хотите избежать копирования.
Параметры: Принимает владеющую коллекцию, реализующую трейт IntoParallelIterator.
Пример:
use rayon::prelude::*;
fn main() {
let vec = vec![1, 2, 3, 4, 5];
// Потребляем вектор и параллельно вычисляем квадраты
let squares: Vec<i32> = vec.into_par_iter().map(|x| x * x).collect();
println!("{:?}", squares); // [1, 4, 9, 16, 25]
// vec больше не доступен!
// println!("{:?}", vec); // Ошибка компиляции
}
4. par_bridge()
Что делает: Преобразует любой последовательный итератор в параллельный. Это мощный инструмент, когда у вас есть итератор, который не является коллекцией (например, генератор или итератор строк из файла).
Параметры: Принимает любой тип, реализующий трейт IntoIterator.
Пример:
use rayon::prelude::*;
fn main() {
// Создаём последовательный итератор с помощью range
let range = 0..100_000;
// Преобразуем его в параллельный и фильтруем чётные числа
let evens: Vec<i32> = range.into_par_iter().filter(|x| x % 2 == 0).collect();
println!("Найдено {} чётных чисел", evens.len()); // 50000
}
5. map()
Что делает: Применяет заданную функцию (замыкание) к каждому элементу итератора, создавая новый итератор с преобразованными значениями. Аналог map из стандартной библиотеки, но параллельный.
Параметры: Принимает замыкание Fn(T) -> U.
Пример:
use rayon::prelude::*;
fn main() {
let data = vec!["1", "2", "3", "4", "5"];
// Параллельно парсим строки в числа
let numbers: Vec<i32> = data.par_iter().map(|s| s.parse::<i32>().unwrap()).collect();
println!("{:?}", numbers); // [1, 2, 3, 4, 5]
}
6. filter()
Что делает: Создаёт новый итератор, который включает только те элементы, для которых заданное замыкание вернуло true. Параллельная версия filter.
Параметры: Принимает замыкание Fn(&T) -> bool.
Пример:
use rayon::prelude::*;
fn main() {
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Параллельно выбираем только чётные числа
let evens: Vec<i32> = numbers.par_iter().filter(|x| *x % 2 == 0).copied().collect();
println!("{:?}", evens); // [2, 4, 6, 8, 10]
}
7. reduce()
Что делает: Сводит элементы итератора к одному значению с помощью ассоциативной операции. Это параллельный аналог fold, но с ключевым отличием: операция должна быть ассоциативной (т.е. (a op b) op c == a op (b op c)), чтобы Rayon мог безопасно выполнять её в произвольном порядке на разных потоках.
Параметры: Принимает два аргумента: identity (нейтральный элемент, например, 0 для сложения) и замыкание Fn(T, T) -> T.
Пример:
use rayon::prelude::*;
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Параллельное суммирование с reduce
let sum = numbers.par_iter().cloned().reduce(|| 0, |a, b| a + b);
println!("Сумма: {}", sum); // 15
}
8. fold()
Что делает: Выполняет свёртку, но в отличие от reduce, позволяет использовать неассоциативные операции и аккумулятор с изменяемым состоянием. Rayon запускает несколько независимых свёрток (по одной на каждый "чанк" данных), а затем объединяет результаты с помощью второго замыкания.
Параметры: Принимает два замыкания: identity (создаёт начальное значение аккумулятора) и fold_op (обновляет аккумулятор), а также reduce_op (объединяет два аккумулятора).
Пример:
use rayon::prelude::*;
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// Параллельно считаем сумму и количество элементов
let (sum, count) = numbers.par_iter().cloned().fold(
|| (0, 0),
|(sum, count), x| (sum + x, count + 1),
|(sum1, count1), (sum2, count2)| (sum1 + sum2, count1 + count2),
);
println!("Сумма: {}, Количество: {}", sum, count); // Сумма: 15, Количество: 5
}
9. for_each()
Что делает: Применяет заданное замыкание к каждому элементу итератора. Используется для выполнения побочных эффектов (например, вывод в консоль, запись в файл