Sincronização em Rust: Por que Mutex e RwLock Importam
A segurança de memória é o principal diferencial de Rust. Enquanto linguagens tradicionais usam locks manuais propensos a deadlocks e race conditions, Rust oferece primitivas de sincronização que garantem segurança em tempo de compilação. Mutex<T> e RwLock<T> são os pilares para trabalhar com dados compartilhados entre threads sem sacrificar performance ou segurança. Nesta aula, você aprenderá quando usar cada um e como evitar armadilhas comuns.
O sistema de ownership do Rust impede que múltiplas threads acessem dados mutáveis simultaneamente sem uma estrutura de sincronização. Mutex e RwLock fornecem essa estrutura, garantindo que apenas uma ou um grupo controlado de threads acesse o recurso por vez.
Entendendo Mutex: Lock Exclusivo
Mutex<T> (mutual exclusion) garante que apenas uma thread acesse o dado protegido por vez. Quando uma thread tenta adquirir o lock, ela bloqueia até conseguir acesso exclusivo. Após o trabalho, o lock é liberado automaticamente quando o MutexGuard sai do escopo.
use std::sync::Mutex;
use std::thread;
use std::sync::Arc;
fn main() {
let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..5 {
let contador_clone = Arc::clone(&contador);
let handle = thread::spawn(move || {
let mut num = contador_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Resultado: {}", *contador.lock().unwrap());
}
Aqui, Arc<Mutex<T>> permite compartilhamento seguro entre threads. Arc (Atomic Reference Counting) garante que o dado vive enquanto alguma thread o referenciar. O lock() retorna um Result contendo um MutexGuard, que implementa Deref para acesso transparente ao dado. Usar unwrap() é aceitável em exemplos, mas em código produção, trate o erro de poisoning (quando uma thread paniqueia enquanto segura o lock).
Armadilhas Comuns com Mutex
Deadlock é o inimigo número um. Se Thread A espera por um lock que Thread B segura, e Thread B espera por um lock que Thread A segura, você tem deadlock. Evite adquirir múltiplos locks em ordens diferentes entre threads. Além disso, Mutex é "tóxico" — se uma thread paniqueia enquanto segura o lock, todas tentativas futuras de lock() falharão.
RwLock: Leitura Paralela, Escrita Exclusiva
RwLock<T> permite múltiplos leitores simultâneos, mas apenas um escritor exclusivo. Use isso quando leituras são muito mais frequentes que escritas — o ganho de performance é significativo comparado a Mutex.
use std::sync::RwLock;
use std::thread;
use std::sync::Arc;
fn main() {
let dados = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Múltiplas threads lendo simultaneamente
for i in 0..3 {
let dados_clone = Arc::clone(&dados);
let handle = thread::spawn(move || {
let leitura = dados_clone.read().unwrap();
println!("Thread {} leu: {:?}", i, *leitura);
// Leitura permanece enquanto leitura está em escopo
});
handles.push(handle);
}
// Uma thread escrevendo
let dados_clone = Arc::clone(&dados);
let write_handle = thread::spawn(move || {
let mut escrita = dados_clone.write().unwrap();
escrita.push(4);
println!("Escrita concluída");
});
handles.push(write_handle);
for handle in handles {
handle.join().unwrap();
}
println!("Dados finais: {:?}", *dados.read().unwrap());
}
read() retorna RwLockReadGuard, permitindo múltiplas simultâneas. write() bloqueia todas as leituras e outras escritas, retornando RwLockWriteGuard com acesso mutável. O overhead de gerenciar múltiplos leitores torna RwLock mais lento para workloads com muita contenção ou escrita frequente.
Quando Usar Cada Um
Escolha Mutex quando escritas e leituras ocorrem com frequência similar, ou quando simplicidade importa mais que performance. É mais leve e menos propenso a deadlocks complexos. Escolha RwLock quando o padrão é claramente leitura-intensivo (ex: cache que raramente muda, configuração do sistema).
// Exemplo: Cache com RwLock
use std::sync::RwLock;
use std::collections::HashMap;
struct Cache {
dados: RwLock<HashMap<String, String>>,
}
impl Cache {
fn get(&self, chave: &str) -> Option<String> {
self.dados.read().unwrap().get(chave).cloned()
}
fn set(&self, chave: String, valor: String) {
self.dados.write().unwrap().insert(chave, valor);
}
}
Segurança e Performance em Prática
Rust garante que você nunca criará race conditions data races com Mutex ou RwLock — o compilador impede acesso direto ao dado sem passar pelo lock. Contudo, a sincronização tem custo: context switches, contenção de cache, overhead de aquisição. O maior ganho vem de arquitetar sua aplicação para minimizar contenção: dividir dados em múltiplos locks, usar canais para comunicação quando apropriado, ou explorar paralelismo sem dados compartilhados.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send(42).unwrap();
});
println!("Recebido: {}", rx.recv().unwrap());
}
Canais (mpsc — multi-producer, single-consumer) são frequentemente melhores que dados compartilhados para comunicação entre threads. Eles oferecem semântica clara de ownership e evitam locks inteiramente para muitos padrões. Considere-os antes de pular direto para Mutex ou RwLock.
Conclusão
Você aprendeu que Mutex