Rayon em Rust: Paralelismo de Dados sem Medo

Guia completo sobre Rayon em Rust: aprenda paralelismo de dados com par_iter, work stealing, benchmarks e exemplos práticos para acelerar seu código.

Introdução

Paralelismo em Rust não precisa ser complicado. Enquanto muitas linguagens exigem gerenciamento manual de threads, locks e sincronização, o Rayon oferece uma abordagem radicalmente simples: troque .iter() por .par_iter() e seu código roda em paralelo — com toda a segurança que o Rust garante em tempo de compilação.

Rayon é uma biblioteca de paralelismo de dados que transforma operações sequenciais em paralelas de forma quase transparente. Com mais de 150 milhões de downloads no crates.io e presente em projetos como ripgrep, cargo e rustfmt, é uma das crates mais confiáveis do ecossistema Rust.

Neste guia, vamos explorar desde os fundamentos até padrões avançados, com benchmarks reais e exemplos práticos.

Paralelismo de Dados vs Paralelismo de Tarefas

Antes de mergulhar no Rayon, é importante entender a diferença entre os dois modelos de paralelismo:

  • Paralelismo de dados: A mesma operação é aplicada simultaneamente a diferentes partes de um conjunto de dados. Exemplo: processar cada pixel de uma imagem ao mesmo tempo.
  • Paralelismo de tarefas: Diferentes operações são executadas simultaneamente. Exemplo: um servidor web processando múltiplas requisições.

Rayon se especializa no primeiro modelo. Para paralelismo de tarefas e I/O assíncrono, Tokio é a escolha certa — veja nosso artigo sobre o ecossistema async em 2026.

Primeiros Passos com Rayon

Adicione Rayon ao seu Cargo.toml:

[dependencies]
rayon = "1.10"

O Básico: par_iter()

use rayon::prelude::*;

fn main() {
    let numeros: Vec<u64> = (1..=10_000_000).collect();

    // Sequencial
    let soma_seq: u64 = numeros.iter()
        .map(|&n| n * n)
        .sum();

    // Paralelo — apenas troque iter() por par_iter()
    let soma_par: u64 = numeros.par_iter()
        .map(|&n| n * n)
        .sum();

    assert_eq!(soma_seq, soma_par);
    println!("Soma dos quadrados: {}", soma_par);
}

É isso. Uma mudança de quatro letras (par_) e seu código agora utiliza todos os cores disponíveis da CPU.

Operações Paralelas Disponíveis

Rayon implementa versões paralelas de todos os adaptadores de iteradores do Rust:

use rayon::prelude::*;

fn main() {
    let dados: Vec<i64> = (-500_000..500_000).collect();

    // map paralelo
    let quadrados: Vec<i64> = dados.par_iter()
        .map(|&x| x * x)
        .collect();

    // filter paralelo
    let positivos: Vec<&i64> = dados.par_iter()
        .filter(|&&x| x > 0)
        .collect();

    // for_each paralelo (efeitos colaterais)
    dados.par_iter()
        .filter(|&&x| x % 100_000 == 0)
        .for_each(|x| println!("Múltiplo encontrado: {}", x));

    // reduce paralelo
    let maximo = dados.par_iter()
        .reduce(|| &i64::MIN, |a, b| if a > b { a } else { b });

    // find_any — retorna QUALQUER match (não necessariamente o primeiro)
    let encontrado = dados.par_iter()
        .find_any(|&&x| x == 42);

    println!("Quadrados: {}, Positivos: {}", quadrados.len(), positivos.len());
    println!("Máximo: {}, Encontrado 42: {:?}", maximo, encontrado);
}

Como o Work Stealing Funciona

Rayon utiliza um work-stealing scheduler para distribuir trabalho entre threads de forma eficiente:

  1. O trabalho é dividido recursivamente em pedaços menores (fork)
  2. Cada thread tem sua própria fila de tarefas
  3. Quando uma thread termina suas tarefas, ela “rouba” trabalho da fila de outra thread
  4. Resultado final é combinado (join)

Esse modelo garante balanceamento de carga automático, mesmo quando as tarefas têm custos variados. Não é necessário particionar manualmente os dados — o Rayon faz isso de forma adaptativa.

Benchmarking: Serial vs Paralelo

Vamos medir a diferença real usando Criterion:

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rayon = "1.10"

[[bench]]
name = "paralelo"
harness = false
// benches/paralelo.rs
use criterion::{criterion_group, criterion_main, Criterion, black_box};
use rayon::prelude::*;

fn processar_sequencial(dados: &[f64]) -> Vec<f64> {
    dados.iter()
        .map(|x| (x.sin() * x.cos()).sqrt().abs())
        .collect()
}

fn processar_paralelo(dados: &[f64]) -> Vec<f64> {
    dados.par_iter()
        .map(|x| (x.sin() * x.cos()).sqrt().abs())
        .collect()
}

fn benchmark(c: &mut Criterion) {
    let dados: Vec<f64> = (0..5_000_000)
        .map(|i| i as f64 * 0.001)
        .collect();

    let mut group = c.benchmark_group("processamento");

    group.bench_function("sequencial", |b| {
        b.iter(|| processar_sequencial(black_box(&dados)))
    });

    group.bench_function("paralelo", |b| {
        b.iter(|| processar_paralelo(black_box(&dados)))
    });

    group.finish();
}

criterion_group!(benches, benchmark);
criterion_main!(benches);

Resultados típicos em uma máquina com 8 cores:

OperaçãoTempo (5M elementos)Speedup
Sequencial~120ms1x
Paralelo (Rayon)~18ms~6.7x

O speedup não é exatamente 8x por causa do overhead de coordenação e da contenção de memória, mas está próximo do ideal teórico.

Ordenação Paralela

Rayon oferece versões paralelas de algoritmos de ordenação:

use rayon::prelude::*;

fn main() {
    let mut dados: Vec<u64> = (0..10_000_000)
        .map(|_| rand::random::<u64>())
        .collect();

    // Ordenação paralela estável
    dados.par_sort();

    // Ordenação paralela instável (mais rápida)
    dados.par_sort_unstable();

    // Ordenação paralela com comparador personalizado
    dados.par_sort_unstable_by(|a, b| b.cmp(a)); // Decrescente

    println!("Primeiros 5: {:?}", &dados[..5]);
}

Para conjuntos grandes (>100K elementos), par_sort_unstable() pode ser 3-4x mais rápido que sort_unstable().

Controlando o Thread Pool

Por padrão, Rayon cria uma thread por core lógico. Você pode personalizar isso:

use rayon::ThreadPoolBuilder;

fn main() {
    // Thread pool global com 4 threads
    ThreadPoolBuilder::new()
        .num_threads(4)
        .build_global()
        .unwrap();

    // Ou criar pools isolados para diferentes cargas de trabalho
    let pool_pesado = ThreadPoolBuilder::new()
        .num_threads(6)
        .thread_name(|i| format!("pesado-{}", i))
        .build()
        .unwrap();

    let resultado = pool_pesado.install(|| {
        let dados: Vec<u64> = (1..=1_000_000).collect();
        dados.par_iter().map(|&n| n * n).sum::<u64>()
    });

    println!("Resultado: {}", resultado);
}

Exemplos Práticos do Mundo Real

Processamento de Imagens

use rayon::prelude::*;

struct Pixel {
    r: u8, g: u8, b: u8,
}

impl Pixel {
    fn escala_cinza(&self) -> u8 {
        ((self.r as f32 * 0.299) +
         (self.g as f32 * 0.587) +
         (self.b as f32 * 0.114)) as u8
    }
}

fn converter_escala_cinza(pixels: &[Pixel]) -> Vec<u8> {
    pixels.par_iter()
        .map(|p| p.escala_cinza())
        .collect()
}

Processamento de Arquivos em Lote

use rayon::prelude::*;
use std::fs;
use std::path::PathBuf;

fn processar_arquivos(caminhos: &[PathBuf]) -> Vec<(PathBuf, usize)> {
    caminhos.par_iter()
        .filter_map(|caminho| {
            let conteudo = fs::read_to_string(caminho).ok()?;
            let contagem = conteudo.lines().count();
            Some((caminho.clone(), contagem))
        })
        .collect()
}

Análise de Dados com Agregação

use rayon::prelude::*;

#[derive(Debug)]
struct Venda {
    produto: String,
    valor: f64,
    quantidade: u32,
}

fn calcular_estatisticas(vendas: &[Venda]) -> (f64, f64, f64) {
    let total: f64 = vendas.par_iter()
        .map(|v| v.valor * v.quantidade as f64)
        .sum();

    let media = total / vendas.len() as f64;

    let max = vendas.par_iter()
        .map(|v| v.valor)
        .reduce(|| f64::MIN, f64::max);

    (total, media, max)
}

Rayon vs std::thread vs Tokio

Escolher a ferramenta certa depende do tipo de trabalho:

CenárioMelhor escolhaPor quê
Processar coleção grandeRayonParalelismo de dados automático
Poucas threads com lógica complexastd::threadControle total
I/O assíncrono (HTTP, DB)TokioNon-blocking I/O
Servidor web com muitas conexõesTokioEscalabilidade de I/O
Pipeline de dados CPU-boundRayonWork stealing eficiente
Background jobs simplesstd::threadSem dependências extras

Para uma comparação mais ampla de concorrência, veja nosso tutorial sobre concorrência em Rust. E se sua carga é I/O-bound, confira o guia do Tokio.

Quando NÃO Usar Rayon

Rayon não é bala de prata. Evite em:

  1. Workloads pequenos: Para vetores com menos de ~10.000 elementos, o overhead de coordenação pode tornar o código paralelo mais lento que o sequencial.

  2. I/O-bound: Rayon é otimizado para CPU-bound. Para operações de I/O (requisições HTTP, leitura de disco), use async/await.

  3. Estado compartilhado com muita contenção: Se cada iteração precisa acessar um Mutex, o lock contention pode eliminar qualquer ganho.

  4. Ordem garantida: par_iter() não garante ordem de processamento. Se a ordem importa, use par_bridge() com cuidado ou mantenha o iterador sequencial.

Paralelismo Aninhado

Rayon lida naturalmente com paralelismo aninhado — chamadas paralelas dentro de chamadas paralelas:

use rayon::prelude::*;

fn main() {
    let matrizes: Vec<Vec<Vec<f64>>> = (0..10)
        .map(|_| {
            (0..1000).map(|_| {
                (0..1000).map(|i| i as f64).collect()
            }).collect()
        })
        .collect();

    // Paralelismo em dois níveis
    let resultados: Vec<Vec<f64>> = matrizes.par_iter()
        .map(|matriz| {
            matriz.par_iter()
                .map(|linha| linha.iter().sum::<f64>())
                .collect()
        })
        .collect();

    println!("Processadas {} matrizes", resultados.len());
}

O scheduler do Rayon automaticamente distribui o trabalho de forma eficiente entre os níveis de paralelismo, sem criar threads excessivas.

Conclusão

Rayon demonstra a filosofia do Rust de oferecer abstrações de custo zero — ou quase zero — que tornam código paralelo tão seguro quanto código sequencial. Com a garantia do compilador contra data races e a ergonomia do par_iter(), não há desculpa para deixar cores ociosos.

Se você está começando com Rust, confira Como Aprender Rust em 2026. Para quem quer dominar o sistema de tipos que torna tudo isso possível, veja nosso tutorial sobre Traits e Generics.

Para entender como a compilação condicional pode otimizar seu código paralelo para diferentes plataformas, leia nosso artigo sobre Compilação Condicional em Rust.

Veja Também

Outras linguagens também oferecem ferramentas para paralelismo: Go tem goroutines com modelo CSP, Zig oferece controle manual de threads com async/await integrado, e Python usa multiprocessing para contornar o GIL.