Rayon en Rust: iteradores paralelos con par_iter, par_sort y ThreadPool

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_iter no 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.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP