Mutex e RwLock em contexto concorrente
1. Introdução à Sincronização de Dados Compartilhados
Em programação concorrente, dois problemas clássicos surgem: corrida de dados (data races) e inconsistência de estado. Quando múltiplas threads acessam e modificam o mesmo dado simultaneamente, o resultado pode ser imprevisível. Rust, com seu sistema de ownership, já previne corridas de dados em tempo de compilação, mas ainda precisamos de mecanismos para compartilhar dados mutáveis entre threads.
O tipo Arc<T> (Atomic Reference Counting) permite que múltiplas threads possuam referências compartilhadas a um mesmo valor, mas não oferece mutabilidade segura — Arc<T> só fornece acesso imutável. Para modificar dados compartilhados, precisamos de primitivas de sincronização como Mutex<T> e RwLock<T>, presentes em std::sync.
2. Mutex: Exclusão Mútua em Rust
Mutex (mutual exclusion) garante que apenas uma thread por vez acesse o dado protegido. Em Rust, seu uso é elegante: o bloqueio é adquirido via lock(), que retorna um MutexGuard<T>. Quando o guardião sai de escopo, o bloqueio é automaticamente liberado graças ao trait Drop.
use std::sync::Mutex;
let contador = Mutex::new(0);
{
let mut guard = contador.lock().unwrap();
*guard += 1;
} // lock liberado aqui
println!("Valor: {}", contador.lock().unwrap());
O método try_lock() tenta adquirir o lock sem bloquear, retornando Err se não for possível. Já o poisoning ocorre se uma thread entra em pânico enquanto segura o lock — o mutex é marcado como "envenenado" e chamadas futuras a lock() retornarão Err.
3. RwLock: Leitura e Escrita com Granularidade Fina
RwLock (Read-Write Lock) oferece uma política mais flexível: múltiplos leitores podem acessar o dado simultaneamente, mas apenas um escritor por vez. Isso é ideal para workloads onde leituras são muito mais frequentes que escritas.
use std::sync::RwLock;
let dados = RwLock::new(vec![1, 2, 3]);
// Múltiplos leitores podem coexistir
let leitor1 = dados.read().unwrap();
let leitor2 = dados.read().unwrap();
println!("Leitores: {:?} e {:?}", leitor1, leitor2);
drop(leitor1);
drop(leitor2);
// Escritor precisa de acesso exclusivo
let mut escritor = dados.write().unwrap();
escritor.push(4);
A API oferece read(), write(), try_read() e try_write(). A versão try_* não bloqueia, útil para evitar deadlocks em cenários complexos.
4. Compartilhamento entre Threads: Arc> e Arc>
Para compartilhar um Mutex ou RwLock entre threads, combinamos com Arc. Exemplo de contador compartilhado:
use std::sync::{Arc, Mutex};
use std::thread;
let contador = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&contador);
handles.push(thread::spawn(move || {
let mut num = c.lock().unwrap();
*num += 1;
}));
}
for h in handles {
h.join().unwrap();
}
println!("Resultado: {}", *contador.lock().unwrap()); // 10
Agora, um cache compartilhado com RwLock:
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
let cache: Arc<RwLock<HashMap<String, i32>>> = Arc::new(RwLock::new(HashMap::new()));
let mut handles = vec![];
// Thread escritora
let c = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let mut map = c.write().unwrap();
map.insert("chave".to_string(), 42);
}));
// Thread leitora
let c = Arc::clone(&cache);
handles.push(thread::spawn(move || {
let map = c.read().unwrap();
if let Some(valor) = map.get("chave") {
println!("Cache: {}", valor);
}
}));
5. Armadilhas e Boas Práticas
Deadlocks ocorrem quando duas threads esperam uma pela outra liberar locks. A solução clássica é estabelecer uma ordem global de aquisição de locks.
// Propenso a deadlock
let a = Mutex::new(1);
let b = Mutex::new(2);
thread::spawn(move || {
let _ga = a.lock().unwrap();
thread::sleep(Duration::from_millis(50));
let _gb = b.lock().unwrap(); // pode deadlock
});
Escopo mínimo do lock: mantenha o guardião vivo pelo menor tempo possível. Use blocos {} para limitar seu escopo.
let mut guard = mutex.lock().unwrap();
*guard += 1;
// Libere explicitamente drop(guard) ou feche o bloco
Poisoning: ao detectar um mutex envenenado, você pode ignorar o erro (com unwrap_or_else) ou tratar o pânico. Em aplicações críticas, considere reinicializar o estado.
6. Mutex vs. RwLock: Quando Usar Cada Um
| Característica | Mutex | RwLock |
|---|---|---|
| Leitores simultâneos | 1 | Múltiplos |
| Escritores simultâneos | 1 | 1 |
| Overhead | Menor | Maior (controle de leitores) |
| Ideal para | Escrita frequente, dados pequenos | Leitura predominante, dados grandes |
Use Mutex quando:
- A maioria das operações envolve escrita
- O dado protegido é pequeno (ex: um contador, uma flag)
- A contenção é baixa ou moderada
Use RwLock quando:
- Leituras são muito mais frequentes que escritas (ex: cache, tabelas de configuração)
- O dado protegido é grande e copiá-lo seria caro
- Você precisa de acesso concorrente de leitura sem bloquear
Trade-off: RwLock tem overhead maior que Mutex devido ao gerenciamento de múltiplos leitores. Em cenários com escrita frequente, Mutex pode ser mais rápido.
7. Alternativas e Considerações Finais
O ecossistema Rust oferece alternativas interessantes:
std::sync::LazyLock(estabilizado no Rust 1.80): para inicialização tardia segura em contexto concorrente.parking_lot::Mutexeparking_lot::RwLock: bibliotecas externas com desempenho superior, sem poisoning e com API mais ergonômica.tokio::sync::Mutex: para uso com async/await, evita bloquear o runtime. Usestd::sync::Mutexapenas em threads blocking ou quando o lock é mantido por tempo muito curto.
Em ambientes assíncronos, std::sync::Mutex pode causar starvation se mantido durante um .await. Prefira tokio::sync::Mutex ou tokio::sync::RwLock para tarefas async.
A escolha entre Mutex e RwLock depende do padrão de acesso aos dados. Para a maioria dos casos, comece com Mutex — é mais simples e eficiente. Migre para RwLock apenas quando medições mostrarem que leituras concorrentes trariam ganhos significativos de desempenho.
Referências
- std::sync::Mutex - Documentação Oficial — Documentação completa da API, incluindo poisoning e exemplos de uso.
- std::sync::RwLock - Documentação Oficial — Referência completa para RwLock, com métodos e exemplos.
- The Rustonomicon - Send and Sync — Explica os traits Send e Sync fundamentais para entender concorrência em Rust.
- Rust by Example - Mutex — Tutorial prático com exemplos passo a passo de uso de Mutex.
- Rust Atomics and Locks - Mara Bos — Livro online gratuito que aprofunda em implementações de locks e atomics em Rust.
- parking_lot crate — Biblioteca popular com implementações mais rápidas de Mutex e RwLock para Rust.
- Tokio Sync - Mutex — Documentação do Mutex assíncrono do Tokio, essencial para programação async.