Threads em Rust: segurança garantida pelo compilador
1. Introdução à Concorrência em Rust
Rust oferece um dos modelos de concorrência mais seguros entre linguagens de programação modernas. A base dessa segurança está no sistema de ownership e borrowing, que já conhecemos do código sequencial. Em Rust, o compilador estende essas garantias para o mundo multithread, prevenindo data races e condições de corrida em tempo de compilação.
A promessa de "fearless concurrency" (concorrência sem medo) significa que você pode escrever código concorrente confiando que o compilador detectará erros como acesso simultâneo a dados mutáveis. Diferente de linguagens como C ou C++, onde esses erros só aparecem em runtime, Rust os captura durante a compilação.
É importante distinguir concorrência (lidar com múltiplas tarefas ao mesmo tempo) de paralelismo (executar múltiplas tarefas simultaneamente). Rust suporta ambos, mas o foco deste artigo é a segurança que o compilador oferece em cenários concorrentes.
2. Criando e Gerenciando Threads com std::thread
A forma mais básica de criar threads em Rust é usando thread::spawn. Closures que capturam dados do escopo externo precisam usar a palavra-chave move para transferir ownership para a nova thread:
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(1));
}
});
for i in 1..3 {
println!("Thread principal: {}", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
Para capturar dados, usamos move:
let dados = vec![1, 2, 3, 4, 5];
let handle = thread::spawn(move || {
println!("Dados recebidos: {:?}", dados);
});
handle.join().unwrap();
// println!("{:?}", dados); // Erro! Ownership foi movido
join() aguarda a thread terminar e retorna um Result que pode conter erros de pânico.
3. Compartilhamento de Dados com Arc<T> e Mutex<T>
Quando precisamos compartilhar dados entre múltiplas threads, usamos Arc<T> (Atomic Reference Counting) para ownership compartilhado e Mutex<T> para acesso mutável seguro:
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let contador = Arc::clone(&contador);
handles.push(thread::spawn(move || {
let mut num = contador.lock().unwrap();
*num += 1;
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Resultado: {}", *contador.lock().unwrap());
}
Arc permite que múltiplas threads possuam referências ao mesmo dado, enquanto Mutex garante que apenas uma thread por vez possa modificar o valor. A combinação Arc<Mutex<T>> é o padrão mais comum para dados mutáveis compartilhados.
4. Comunicação entre Threads com Canais (mpsc)
O módulo std::sync::mpsc implementa canais para comunicação entre threads no padrão produtor-consumidor:
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let mensagens = vec!["olá", "mundo", "rust"];
for msg in mensagens {
tx.send(msg).unwrap();
thread::sleep(std::time::Duration::from_secs(1));
}
});
for recebida in rx {
println!("Recebido: {}", recebida);
}
}
Para múltiplos produtores, clonamos o Sender:
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
tx.send("da thread 1").unwrap();
});
thread::spawn(move || {
tx1.send("da thread 2").unwrap();
});
for msg in rx {
println!("{}", msg);
}
5. Garantias de Segurança do Compilador
O compilador Rust previne data races através do borrow checker. Os traits Send e Sync são fundamentais:
Send: tipos que podem ter ownership transferido entre threads com segurançaSync: tipos que podem ser referenciados por múltiplas threads simultaneamente
A maioria dos tipos implementa esses traits automaticamente, mas tipos como Rc<T> não são Send:
use std::rc::Rc;
use std::thread;
fn main() {
let dados = Rc::new(5);
thread::spawn(move || {
println!("{}", dados); // Erro! Rc não implementa Send
});
}
Este código não compila porque Rc não é seguro para threads. O compilador nos força a usar Arc no lugar.
6. Padrões Avançados com RwLock<T> e Condvar
RwLock permite múltiplos leitores ou um único escritor:
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let cache = Arc::new(RwLock::new(String::from("dados iniciais")));
let mut handles = vec![];
// Leitores concorrentes
for _ in 0..5 {
let cache = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let leitura = cache.read().unwrap();
println!("Lendo: {}", *leitura);
}));
}
// Escritor exclusivo
let cache = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let mut escrita = cache.write().unwrap();
*escrita = String::from("dados atualizados");
}));
for handle in handles {
handle.join().unwrap();
}
}
7. Threads com Escopo (Scoped Threads)
Desde Rust 1.63, thread::scope permite criar threads que podem emprestar dados sem precisar de Arc:
use std::thread;
fn main() {
let dados = vec![1, 2, 3, 4, 5];
thread::scope(|s| {
s.spawn(|| {
println!("Thread 1: {:?}", &dados);
});
s.spawn(|| {
println!("Thread 2: {:?}", &dados);
});
});
println!("Dados ainda disponíveis: {:?}", dados);
}
As threads com escopo garantem que todas terminam antes do escopo fechar, permitindo referências seguras sem overhead de contagem de referências.
8. Boas Práticas e Armadilhas Comuns
Evitando deadlocks: mantenha uma ordem consistente ao adquirir múltiplos locks e use try_lock quando apropriado:
use std::sync::{Mutex, MutexGuard};
fn transferir(origem: &Mutex<i32>, destino: &Mutex<i32>, valor: i32) {
let (primeiro, segundo) = if std::ptr::eq(origem, destino) {
(origem, destino)
} else if std::ptr::addr_of!(*origem) < std::ptr::addr_of!(*destino) {
(origem, destino)
} else {
(destino, origem)
};
let mut a = primeiro.lock().unwrap();
let mut b = segundo.lock().unwrap();
*a -= valor;
*b += valor;
}
Performance: criar threads tem custo. Para tarefas curtas, use thread pools com crates como rayon:
use rayon::prelude::*;
fn main() {
let numeros: Vec<i32> = (0..1000).collect();
let soma: i32 = numeros.par_iter().sum();
println!("Soma paralela: {}", soma);
}
Testando concorrência: ferramentas como loom ajudam a detectar bugs em código concorrente durante testes.
Referências
- The Rust Programming Language - Fearless Concurrency — Capítulo oficial sobre concorrência em Rust, cobrindo threads, canais e estado compartilhado
- Rust Reference - std::thread module — Documentação completa do módulo std::thread com exemplos
- Rust Reference - Send and Sync traits — Explicação detalhada sobre os traits Send e Sync no Rustonomicon
- Rust by Example - Scoped Threads — Exemplos práticos de threads com escopo e comunicação entre threads
- Loom: Testing concurrent Rust code — Crate para testes de concorrência com modelos de memória
- Rayon: Data parallelism in Rust — Documentação do Rayon para paralelismo de dados com thread pools