Rayon es la biblioteca de paralelismo de datos más usada en el ecosistema Rust. Su API imita la de los iteradores estándar: en la mayoría de los casos basta con cambiar .iter() por .par_iter() para distribuir el trabajo entre todos los núcleos disponibles. Rayon gestiona automáticamente un pool de hilos y el reparto de tareas mediante work-stealing.
De iter() a par_iter(): el cambio mínimo
Rayon implementa los mismos adaptadores que los iteradores estándar (map, filter, sum, collect
). El compilador garantiza que las closures pasadas a Rayon sean Send; si no lo son, obtendrás un error claro en compilación, no una carrera de datos en runtime.
[dependencies]
rayon = "1.10"
use rayon::prelude::*;
fn es_primo(n: u64) -> bool {
if n < 2 { return false; }
if n == 2 { return true; }
if n % 2 == 0 { return false; }
let raiz = (n as f64).sqrt() as u64 + 1;
(3..raiz).step_by(2).all(|i| n % i != 0)
}
fn main() {
let rango: Vec<u64> = (2..=1_000_000).collect();
// Versión secuencial
let primos_seq: Vec<u64> = rango.iter()
.filter(|&&n| es_primo(n))
.copied()
.collect();
// Versión paralela: cambio mínimo
let primos_par: Vec<u64> = rango.par_iter()
.filter(|&&n| es_primo(n))
.copied()
.collect();
println!("Primos hasta 1M: {}", primos_seq.len());
println!("Paralelo coincide: {}", primos_seq == primos_par);
}
par_sort: ordenación paralela
Rayon también ofrece par_sort y par_sort_by para ordenar slices en paralelo usando merge-sort multi-hilo. Para vectores grandes supera al sort estándar en casi todos los benchmarks modernos.
use rayon::prelude::*;
fn main() {
let mut datos: Vec<f64> = (0..1_000_000)
.map(|i| (i as f64 * 0.123456789).sin())
.collect();
// Ordenación paralela in-place
datos.par_sort_by(|a, b| a.partial_cmp(b).unwrap());
println!("Primero: {:.6}", datos[0]);
println!("Último: {:.6}", datos[datos.len() - 1]);
// par_sort_unstable_by es más rápida cuando el orden entre iguales no importa
let mut ids: Vec<u32> = (0..500_000).rev().collect();
ids.par_sort_unstable();
println!("Primer id: {}", ids[0]); // 0
}
rayon::join para paralelismo de tareas
rayon::join ejecuta dos closures en paralelo y espera a que ambas terminen. Es el equivalente paralelo de llamar dos funciones en secuencia, útil para dividir problemas en subtareas independientes.
use rayon::prelude::*;
fn suma_rango(datos: &[i64]) -> i64 {
if datos.len() <= 1000 {
return datos.iter().sum();
}
let mitad = datos.len() / 2;
let (izq, der) = datos.split_at(mitad);
let (s_izq, s_der) = rayon::join(
|| suma_rango(izq),
|| suma_rango(der),
);
s_izq + s_der
}
fn main() {
let datos: Vec<i64> = (1..=10_000_000).collect();
let suma = suma_rango(&datos);
let esperada: i64 = 10_000_000 * 10_000_001 / 2;
println!("Suma: {suma}, correcta: {}", suma == esperada);
}
ThreadPoolBuilder: controlar el pool de hilos
Por defecto Rayon crea un pool con un hilo por núcleo lógico. ThreadPoolBuilder permite ajustar ese número, asignar nombres a los hilos para depuración, o crear pools independientes para distintos subsistemas.
use rayon::ThreadPoolBuilder;
fn main() {
// Pool con 4 hilos fijos, independiente del pool global
let pool = ThreadPoolBuilder::new()
.num_threads(4)
.thread_name(|i| format!("worker-{i}"))
.build()
.unwrap();
let resultado = pool.install(|| {
(1u64..=1_000_000)
.into_par_iter()
.filter(|n| n % 7 == 0)
.sum::<u64>()
});
println!("Suma de múltiplos de 7 hasta 1M: {resultado}");
// Número de hilos del pool global (por defecto = núcleos lógicos)
println!("Pool global: {} hilos", rayon::current_num_threads());
}
Cuándo Rayon no es la solución
Rayon no es útil en todos los casos. Evítalo cuando:
- Los elementos son pocos (menos de ~10 000 o el trabajo por elemento es trivial): el overhead del scheduling supera el beneficio.
- El trabajo es I/O-bound (red, disco): Rayon no mejora el throughput de I/O; usa Tokio o async/await.
- El orden de los resultados importa y no puedes reordenarlos después:
par_iterno garantiza orden. - Las closures capturan estado no
Send: el compilador lo rechazará.
Para trabajo CPU-bound con colecciones grandes, Rayon es difícilmente superable en Rust. La combinación de su API sin fricciones y las garantías del compilador lo convierten en la opción por defecto antes de explorar paralelismo más bajo nivel con hilos manuales.
