Concorrência é um dos territórios mais perigosos da programação. Data races, deadlocks, condições de corrida — esses bugs são notoriamente difíceis de reproduzir, diagnosticar e corrigir. Linguagens como C e C++ oferecem threads poderosas mas sem garantias: o compilador não te avisa quando você está criando uma condição de corrida.
Rust faz uma promessa ousada: concorrência sem medo. O sistema de tipos — ownership, borrowing e os traits Send e Sync — torna impossível compilar código com data races. Não é uma questão de disciplina ou cuidado. É uma garantia do compilador.
Neste artigo vamos explorar threads nativas, comunicação por canais, e estado compartilhado seguro com Mutex e Arc.
Criando threads
Threads em Rust são criadas com std::thread::spawn. A closure passada para spawn é executada em uma nova thread do sistema operacional:
use std::thread;
use std::time::Duration;
fn main() {
let handle = thread::spawn(|| {
for i in 1..=5 {
println!("Thread filha: {i}");
thread::sleep(Duration::from_millis(50));
}
});
for i in 1..=3 {
println!("Thread principal: {i}");
thread::sleep(Duration::from_millis(80));
}
// join() aguarda a thread filha terminar
handle.join().unwrap();
println!("Ambas as threads concluíram.");
}
Saída (ordem pode variar):
Thread principal: 1
Thread filha: 1
Thread filha: 2
Thread principal: 2
Thread filha: 3
Thread filha: 4
Thread principal: 3
Thread filha: 5
Ambas as threads concluíram.
thread::spawn retorna um JoinHandle — um handle para a thread. Chamar .join() bloqueia a thread atual até que a thread filha termine. Sem join, a thread principal pode encerrar antes da filha, matando-a prematuramente.
move closures em threads
Para usar dados da thread principal dentro de uma thread filha, a closure precisa tomar posse desses dados com move:
use std::thread;
fn main() {
let dados = vec![1, 2, 3, 4, 5];
// sem move: ERRO — compilador não sabe se dados
// ainda existirá quando a thread rodar
let handle = thread::spawn(move || {
println!("Dados na thread filha: {:?}", dados);
let soma: i32 = dados.iter().sum();
println!("Soma: {soma}");
});
// dados foi movido para a thread — não pode mais ser usado aqui
// println!("{:?}", dados); // ERRO de compilação
handle.join().unwrap();
}
O move é obrigatório aqui — sem ele, o compilador recusa porque não pode garantir que dados viverá pelo menos tanto quanto a thread. Com move, a thread toma posse de dados e é responsável pela sua destruição.
Retornando valores de threads
Threads podem retornar valores através do JoinHandle:
use std::thread;
fn calcular_em_paralelo(dados: Vec<Vec<i64>>) -> Vec<i64> {
let handles: Vec<_> = dados
.into_iter()
.map(|chunk| {
thread::spawn(move || {
chunk.iter().sum::<i64>()
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().unwrap())
.collect()
}
fn main() {
let dados = vec![
vec![1, 2, 3, 4, 5],
vec![10, 20, 30, 40, 50],
vec![100, 200, 300, 400, 500],
];
let somas = calcular_em_paralelo(dados);
println!("Somas parciais: {:?}", somas); // [15, 150, 1500]
let total: i64 = somas.iter().sum();
println!("Total: {total}"); // 1665
}
Canais — comunicação entre threads
A abordagem mais idiomática para comunicação entre threads em Rust é o modelo de canais — inspirado na filosofia Go de "não compartilhe memória para comunicar; comunique para compartilhar memória."
Um canal tem dois lados: um transmissor (Sender) e um receptor (Receiver). Dados fluem do transmissor para o receptor:
use std::sync::mpsc; // multiple producer, single consumer
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let mensagens = vec!["olá", "do", "outro", "lado"];
for msg in mensagens {
tx.send(msg).unwrap();
println!("Enviado: {msg}");
}
});
// rx.recv() bloqueia até receber uma mensagem
for recebido in rx {
println!("Recebido: {recebido}");
}
println!("Canal fechado.");
}
O loop for recebido in rx itera sobre o receptor até que o canal seja fechado — o que acontece automaticamente quando todos os transmissores são destruídos.
Múltiplos produtores
O mpsc suporta múltiplos transmissores através de clone:
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
// Três threads produtoras
for id in 0..3 {
let tx_clone = tx.clone();
thread::spawn(move || {
for i in 0..3 {
let msg = format!("Thread {id} — mensagem {i}");
tx_clone.send(msg).unwrap();
thread::sleep(Duration::from_millis(10));
}
});
}
// Necessário: descartamos o tx original para que
// o canal feche quando todas as cópias forem destruídas
drop(tx);
let mut total = 0;
for msg in rx {
println!("{msg}");
total += 1;
}
println!("Total de mensagens recebidas: {total}"); // 9
}
O drop(tx) é crucial — sem ele, o canal nunca fecha, pois o transmissor original ainda existe na thread principal.
Estado compartilhado com Arc>
Canais são ótimos para comunicação unidirecional. Mas às vezes você precisa que múltiplas threads compartilhem e modifiquem o mesmo dado. Para isso, Rust oferece Arc>:
Arc— comoRc, mas thread-safe (Atomic Reference Counted)Mutex— garante que apenas uma thread acessa o dado por vez
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0i32));
let mut handles = Vec::new();
for _ in 0..10 {
let contador_clone = Arc::clone(&contador);
let handle = thread::spawn(move || {
let mut valor = contador_clone.lock().unwrap();
*valor += 1;
// lock liberado automaticamente quando `valor` sai de escopo
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Contador final: {}", *contador.lock().unwrap()); // 10
}
lock() bloqueia a thread atual até que o mutex esteja disponível. Retorna um MutexGuard — um smart pointer que libera o lock automaticamente quando sai de escopo. Isso torna impossível esquecer de liberar o lock — um bug clássico em outras linguagens.
Um exemplo mais completo: pool de trabalho
Vamos implementar um pool de threads que processa tarefas em paralelo:
use std::sync::{Arc, Mutex};
use std::thread;
struct PoolDeTrabalho {
handles: Vec<thread::JoinHandle<()>>,
sender: std::sync::mpsc::Sender<Box<dyn FnOnce() + Send + 'static>>,
}
impl PoolDeTrabalho {
fn novo(tamanho: usize) -> Self {
let (tx, rx) = std::sync::mpsc::channel::<Box<dyn FnOnce() + Send + 'static>>();
let rx = Arc::new(Mutex::new(rx));
let mut handles = Vec::new();
for id in 0..tamanho {
let rx_clone = Arc::clone(&rx);
let handle = thread::spawn(move || {
loop {
let tarefa = {
let lock = rx_clone.lock().unwrap();
lock.recv()
};
match tarefa {
Ok(f) => {
println!(" Worker {id} processando...");
f();
}
Err(_) => {
println!(" Worker {id} encerrando.");
break;
}
}
}
});
handles.push(handle);
}
PoolDeTrabalho { handles, sender: tx }
}
fn executar<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
self.sender.send(Box::new(f)).unwrap();
}
fn aguardar(self) {
drop(self.sender); // fecha o canal
for handle in self.handles {
handle.join().unwrap();
}
}
}
fn main() {
println!("Iniciando pool com 3 workers...
");
let pool = PoolDeTrabalho::novo(3);
let resultados = Arc::new(Mutex::new(Vec::new()));
for i in 0..9 {
let resultados_clone = Arc::clone(&resultados);
pool.executar(move || {
// Simula trabalho
let resultado = i * i;
thread::sleep(std::time::Duration::from_millis(50));
resultados_clone.lock().unwrap().push((i, resultado));
});
}
pool.aguardar();
let mut res = resultados.lock().unwrap();
res.sort_by_key(|&(i, _)| i);
println!("
── Resultados ──");
for (i, r) in res.iter() {
println!(" {i}² = {r}");
}
}
Os traits Send e Sync
A garantia de concorrência segura de Rust é implementada por dois traits especiais do compilador:
Send — um tipo que implementa Send pode ser transferido entre threads. Arc é Send. Rc não é — não seria seguro transferir um Rc para outra thread porque o contador de referências não é atômico.
Sync — um tipo que implementa Sync pode ser referenciado por múltiplas threads simultaneamente. Mutex é Sync. RefCell não é — suas verificações de borrow em runtime não são thread-safe.
Esses traits são implementados automaticamente pelo compilador para tipos que satisfazem as condições. Se você tentar enviar um Rc para outra thread, o compilador recusa com uma mensagem clara explicando que Rc não é Send.
use std::rc::Rc;
use std::thread;
fn main() {
let rc = Rc::new(42);
// ERRO de compilação: Rc não implementa Send
// thread::spawn(move || {
// println!("{}", rc);
// });
// CORRETO: Arc é Send
let arc = std::sync::Arc::new(42);
thread::spawn(move || {
println!("{}", arc);
}).join().unwrap();
}
Paralelismo com rayon
Para paralelismo de dados — processar coleções em paralelo — a crate rayon é a solução idiomática. Ela implementa um pool de threads inteligente e permite converter iteradores sequenciais em paralelos com uma única mudança:
Adicione ao Cargo.toml:
[dependencies]
rayon = "1.8"
use rayon::prelude::*;
fn e_primo(n: u64) -> bool {
if n < 2 { return false; }
if n == 2 { return true; }
if n % 2 == 0 { return false; }
let limite = (n as f64).sqrt() as u64;
(3..=limite).step_by(2).all(|i| n % i != 0)
}
fn main() {
let numeros: Vec<u64> = (2..=100_000).collect();
// Sequencial
let inicio = std::time::Instant::now();
let primos_seq: Vec<u64> = numeros.iter()
.cloned()
.filter(|&n| e_primo(n))
.collect();
let tempo_seq = inicio.elapsed();
// Paralelo — apenas .par_iter() em vez de .iter()
let inicio = std::time::Instant::now();
let primos_par: Vec<u64> = numeros.par_iter()
.cloned()
.filter(|&n| e_primo(n))
.collect();
let tempo_par = inicio.elapsed();
println!("Primos encontrados: {}", primos_seq.len());
println!("Tempo sequencial : {:?}", tempo_seq);
println!("Tempo paralelo : {:?}", tempo_par);
println!("Speedup : {:.1}x",
tempo_seq.as_secs_f64() / tempo_par.as_secs_f64());
// Resultados são os mesmos (mas ordem pode diferir no paralelo)
assert_eq!(primos_seq.len(), primos_par.len());
}
Saída típica em máquina com 8 núcleos:
Primos encontrados: 9592
Tempo sequencial : 45ms
Tempo paralelo : 8ms
Speedup : 5.6x
RwLock — múltiplos leitores ou um escritor
Mutex permite apenas um acesso por vez. RwLock é mais refinado: permite múltiplos leitores simultâneos, mas apenas um escritor por vez:
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let dados = Arc::new(RwLock::new(vec![1, 2, 3, 4, 5]));
let mut handles = Vec::new();
// 5 threads leitoras — podem ler simultaneamente
for id in 0..5 {
let dados_clone = Arc::clone(&dados);
handles.push(thread::spawn(move || {
let leitura = dados_clone.read().unwrap();
println!("Leitor {id}: soma = {}", leitura.iter().sum::<i32>());
}));
}
for h in handles { h.join().unwrap(); }
// 1 thread escritora — acesso exclusivo
let dados_clone = Arc::clone(&dados);
thread::spawn(move || {
let mut escrita = dados_clone.write().unwrap();
escrita.push(6);
escrita.push(7);
println!("Escritor adicionou elementos.");
}).join().unwrap();
println!("Dados finais: {:?}", dados.read().unwrap());
}
Use RwLock quando leituras são muito mais frequentes que escritas — como caches, configurações e índices.
Um programa completo: simulador de mercado
Vamos combinar tudo em uma simulação de mercado com múltiplos agentes operando em paralelo:
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
#[derive(Debug)]
struct Mercado {
preco: f64,
volume_total: u64,
historico: Vec<f64>,
}
impl Mercado {
fn novo(preco_inicial: f64) -> Self {
Mercado {
preco: preco_inicial,
volume_total: 0,
historico: vec![preco_inicial],
}
}
fn comprar(&mut self, agente: &str, quantidade: u64) {
let variacao = 1.0 + (quantidade as f64 * 0.001);
self.preco *= variacao;
self.volume_total += quantidade;
self.historico.push(self.preco);
println!(" {agente} COMPROU {quantidade} → preço: R${:.2}",
self.preco);
}
fn vender(&mut self, agente: &str, quantidade: u64) {
let variacao = 1.0 - (quantidade as f64 * 0.001);
self.preco *= variacao;
self.volume_total += quantidade;
self.historico.push(self.preco);
println!(" {agente} VENDEU {quantidade} → preço: R${:.2}",
self.preco);
}
}
fn agente_comprador(mercado: Arc<Mutex<Mercado>>, nome: String, operacoes: u32) {
for i in 0..operacoes {
thread::sleep(Duration::from_millis(20));
let mut m = mercado.lock().unwrap();
m.comprar(&nome, (i as u64 + 1) * 10);
}
}
fn agente_vendedor(mercado: Arc<Mutex<Mercado>>, nome: String, operacoes: u32) {
for i in 0..operacoes {
thread::sleep(Duration::from_millis(25));
let mut m = mercado.lock().unwrap();
m.vender(&nome, (i as u64 + 1) * 8);
}
}
fn main() {
let mercado = Arc::new(Mutex::new(Mercado::novo(100.0)));
println!("── Simulação de Mercado ──");
println!("Preço inicial: R$100.00
");
let mut handles = Vec::new();
// 2 compradores
for i in 1..=2 {
let m = Arc::clone(&mercado);
handles.push(thread::spawn(move || {
agente_comprador(m, format!("Comprador{i}"), 3);
}));
}
// 2 vendedores
for i in 1..=2 {
let m = Arc::clone(&mercado);
handles.push(thread::spawn(move || {
agente_vendedor(m, format!("Vendedor{i}"), 3);
}));
}
for h in handles { h.join().unwrap(); }
let m = mercado.lock().unwrap();
let preco_inicial = m.historico[0];
let preco_final = m.preco;
let variacao = (preco_final - preco_inicial) / preco_inicial * 100.0;
println!("
── Relatório Final ──");
println!("Preço inicial : R${preco_inicial:.2}");
println!("Preço final : R${preco_final:.2}");
println!("Variação : {variacao:+.2}%");
println!("Volume total : {} operações", m.volume_total);
println!("Operações : {}", m.historico.len() - 1);
let max = m.historico.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min = m.historico.iter().cloned().fold(f64::INFINITY, f64::min);
println!("Máximo : R${max:.2}");
println!("Mínimo : R${min:.2}");
}
Saída (ordem das operações varia):
── Simulação de Mercado ──
Preço inicial: R$100.00
Comprador1 COMPROU 10 → preço: R$101.00
Vendedor1 VENDEU 8 → preço: R$100.19
Comprador2 COMPROU 10 → preço: R$101.20
...
── Relatório Final ──
Preço inicial : R$100.00
Preço final : R$103.45
Variação : +3.45%
Volume total : 144 operações
Operações : 12
Máximo : R$108.23
Mínimo : R$99.87
O que "concorrência sem medo" significa na prática
Rust não elimina deadlocks — esses ainda são possíveis e requerem cuidado de design. O que Rust elimina é a classe mais devastadora de bugs concorrentes: data races — acessos simultâneos a memória onde pelo menos um é uma escrita.
Em C++, você pode compilar e executar código com data race sem nenhum aviso. O resultado é comportamento indefinido — o programa pode parecer funcionar, corromper dados silenciosamente, ou crashar aleatoriamente em produção.
Em Rust, o compilador rejeita código que poderia criar data races. Se compilou, não tem data race. Essa garantia é absoluta — não é uma questão de boas práticas, é uma propriedade do sistema de tipos.
Isso é o que torna Rust a linguagem de escolha crescente para sistemas onde concorrência e confiabilidade são requisitos: kernels, servidores de alta performance, sistemas embarcados críticos.
Fontes e leituras recomendadas
- The Rust Programming Language, Cap. 16 — Fearless Concurrency — https://doc.rust-lang.org/book/ch16-00-concurrency.html
- Rust by Example — Threads — https://doc.rust-lang.org/rust-by-example/std_misc/threads.html
rayon— Data Parallelism in Rust — https://docs.rs/rayoncrossbeamcrate — canais e primitivas de concorrência avançadas — https://docs.rs/crossbeam- "Fearless Concurrency with Rust" — Aaron Turon — artigo original do blog da Mozilla — https://blog.rust-lang.org/2015/04/10/Fearless-Concurrency.html
- "The Rustonomicon" — cap. sobre Send e Sync — https://doc.rust-lang.org/nomicon/send-and-sync.html
- Jon Gjengset — "Crust of Rust: Atomics and Memory Ordering" — https://www.youtube.com/watch?v=rMGWeSjctlY