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:
- O trabalho é dividido recursivamente em pedaços menores (fork)
- Cada thread tem sua própria fila de tarefas
- Quando uma thread termina suas tarefas, ela “rouba” trabalho da fila de outra thread
- 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ção | Tempo (5M elementos) | Speedup |
|---|---|---|
| Sequencial | ~120ms | 1x |
| 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ário | Melhor escolha | Por quê |
|---|---|---|
| Processar coleção grande | Rayon | Paralelismo de dados automático |
| Poucas threads com lógica complexa | std::thread | Controle total |
| I/O assíncrono (HTTP, DB) | Tokio | Non-blocking I/O |
| Servidor web com muitas conexões | Tokio | Escalabilidade de I/O |
| Pipeline de dados CPU-bound | Rayon | Work stealing eficiente |
| Background jobs simples | std::thread | Sem 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:
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.
I/O-bound: Rayon é otimizado para CPU-bound. Para operações de I/O (requisições HTTP, leitura de disco), use async/await.
Estado compartilhado com muita contenção: Se cada iteração precisa acessar um Mutex, o lock contention pode eliminar qualquer ganho.
Ordem garantida:
par_iter()não garante ordem de processamento. Se a ordem importa, usepar_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.