Arc e Mutex
Esta aula aborda o compartilhamento de dados entre threads em Rust usando Arc e Mutex. Você aprenderá a diferença entre Arc e Rc, como usar Mutex com proteção contra poisoning e como evitar deadlocks.
Em Rust, a segurança de memória é garantida em tempo de compilação, mas isso impõe desafios para o compartilhamento de dados entre threads. Nesta aula, exploraremos duas ferramentas essenciais: Arc (Atomic Reference Counting) e Mutex (Mutual Exclusion). Elas permitem que múltiplas threads acessem dados mutáveis de forma segura, sem violar as regras de ownership e borrowing do Rust.
Começaremos entendendo como Arc possibilita o compartilhamento de ownership imutável entre threads, enquanto Mutex fornece acesso mutável controlado. Veremos também armadilhas comuns como deadlocks e o fenômeno de poisoning de mutexes.
Compartilhamento entre threads
Para compartilhar dados entre threads, precisamos de um tipo que implemente Send e Sync. Arc é um contador de referências atômico, que pode ser usado em múltiplas threads. Ele fornece ownership compartilhado imutável: você pode clonar um Arc e passar a cópia para outra thread, e o dado será liberado quando o último Arc for destruído.
No entanto, Arc só permite acesso imutável ao dado interno. Para modificar o dado, precisamos combiná-lo com Mutex ou RwLock. Exemplo básico:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());Arc vs Rc
Rc (Reference Counting) é usado para compartilhamento de ownership dentro de uma única thread. Ele não é Send nem Sync, pois o contador de referências não é atômico e não é seguro para uso concorrente. Arc é a versão atômica de Rc, que pode ser usada entre threads. A diferença principal é que Arc usa operações atômicas para incrementar/decrementar o contador, o que tem um custo de performance maior, mas é necessário para segurança em concorrência.
Tabela comparativa:
Rc: Não éSendnemSync. Uso: single-thread.Arc: ÉSendeSync. Uso: multi-thread.- Ambos permitem acesso imutável compartilhado; para mutabilidade, combine com
RefCell(Rc) ouMutex/RwLock(Arc).
Exemplo de uso de Rc (não compila em contexto multi-thread):
use std::rc::Rc;
use std::thread;
let rc = Rc::new(5);
let rc2 = Rc::clone(&rc);
thread::spawn(move || { // erro: Rc não implementa Send
println!("{}", rc2);
});Mutex e poisoning
Mutex fornece exclusão mútua: apenas uma thread pode acessar o dado protegido por vez. A chamada lock() retorna um MutexGuard que desbloqueia automaticamente ao sair de escopo. Se uma thread entrar em pânico enquanto detém o lock, o mutex fica poisoned (envenenado). Isso significa que futuras tentativas de lock retornarão um erro (PoisonError).
O poisoning é um mecanismo de segurança: se uma thread corrompeu o dado, outras threads são notificadas. Você pode lidar com isso usando lock().unwrap() (que propaga o pânico) ou tratando o erro explicitamente. Exemplo de poisoning:
use std::sync::{Arc, Mutex};
use std::thread;
let mutex = Arc::new(Mutex::new(0));
let mutex2 = Arc::clone(&mutex);
let handle = thread::spawn(move || {
let _guard = mutex2.lock().unwrap();
panic!(); // envenena o mutex
});
handle.join().ok(); // ignora o pânico
let result = mutex.lock(); // retorna Err(PoisonError)
match result {
Ok(val) => println!("Value: {}", val),
Err(poisoned) => {
println!("Mutex poisoned, recovering...");
let val = poisoned.into_inner();
println!("Value: {}", val);
}
}Deadlocks
Deadlock ocorre quando duas ou mais threads ficam esperando indefinidamente por locks que nunca serão liberados. Um exemplo clássico: thread A segura lock X e espera lock Y; thread B segura lock Y e espera lock X. Em Rust, deadlocks podem acontecer se você tentar obter dois locks em ordem diferente em threads diferentes.
Para evitar deadlocks, siga estas práticas:
- Adquira locks sempre na mesma ordem em todas as threads.
- Use
try_lockcom timeout para evitar espera infinita. - Mantenha o escopo do lock o menor possível.
- Considere usar
RwLockse leituras forem mais frequentes que escritas.
Exemplo de deadlock:
use std::sync::{Arc, Mutex};
use std::thread;
let lock1 = Arc::new(Mutex::new(()));
let lock2 = Arc::new(Mutex::new(()));
let l1 = Arc::clone(&lock1);
let l2 = Arc::clone(&lock2);
let handle1 = thread::spawn(move || {
let _g1 = l1.lock().unwrap();
thread::sleep(std::time::Duration::from_millis(10));
let _g2 = l2.lock().unwrap(); // deadlock se thread2 segura lock2
});
let l1 = Arc::clone(&lock1);
let l2 = Arc::clone(&lock2);
let handle2 = thread::spawn(move || {
let _g2 = l2.lock().unwrap();
thread::sleep(std::time::Duration::from_millis(10));
let _g1 = l1.lock().unwrap(); // deadlock
});
handle1.join().unwrap();
handle2.join().unwrap();Para evitar, adquira os locks na mesma ordem (ex.: sempre lock1 antes de lock2).
Boas práticas
- Prefira Arc<Mutex<T>> para dados mutáveis compartilhados. Para dados imutáveis, apenas Arc<T> é suficiente.
- Use RwLock se houver muitas leituras e poucas escritas.
- Sempre trate o erro de lock() adequadamente; não ignore o poisoning.
- Mantenha o lock o menor tempo possível: evite operações lentas dentro do lock.
- Documente a ordem de aquisição de locks para evitar deadlocks.
Referências
- Documentação oficial de Arc
- Documentação oficial de Mutex
- The Rust Book: Shared State
- Rustonomicon: Concurrency
- Too Many Lists: Arc and Mutex
Exercícios
Crie um programa que usa
Arc<Mutex<Vec<i32>>>para que 5 threads adicionem cada uma 1000 elementos ao vetor. Ao final, o vetor deve ter 5000 elementos.✓ Resposta:use std::sync::{Arc, Mutex}; use std::thread; fn main() { let data = Arc::new(Mutex::new(Vec::new())); let mut handles = vec![]; for _ in 0..5 { let data = Arc::clone(&data); let handle = thread::spawn(move || { for i in 0..1000 { let mut vec = data.lock().unwrap(); vec.push(i); } }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Length: {}", data.lock().unwrap().len()); }Explique por que o código abaixo não compila e corrija-o usando
Arc.use std::rc::Rc; use std::thread; fn main() { let rc = Rc::new(5); let rc2 = rc.clone(); thread::spawn(move || { println!("{}", rc2); }); }✓ Resposta: O código não compila porqueRcnão implementaSend. Para compartilhar entre threads, useArc.use std::sync::Arc; use std::thread; fn main() { let arc = Arc::new(5); let arc2 = Arc::clone(&arc); thread::spawn(move || { println!("{}", arc2); }).join().unwrap(); }O que é poisoning em um Mutex? Como você pode recuperar o valor de um Mutex envenenado?
✓ Resposta: Poisoning ocorre quando uma thread entra em pânico enquanto detém um lock. O mutex fica envenenado e futuras chamadas alock()retornamErr(PoisonError). Para recuperar o valor, usepoisoned.into_inner()no erro, que retorna o dado interno (sem lock).Escreva um programa que cria um deadlock intencional entre duas threads usando dois mutexes. Explique como corrigi-lo.
✓ Resposta: Exemplo de deadlock:
Correção: ambas as threads adquirem os locks na mesma ordem (ex.: m1 antes de m2).use std::sync::{Arc, Mutex}; use std::thread; fn main() { let m1 = Arc::new(Mutex::new(())); let m2 = Arc::new(Mutex::new(())); let m1_clone = Arc::clone(&m1); let m2_clone = Arc::clone(&m2); let handle1 = thread::spawn(move || { let _g1 = m1_clone.lock().unwrap(); thread::sleep(std::time::Duration::from_millis(10)); let _g2 = m2_clone.lock().unwrap(); }); let handle2 = thread::spawn(move || { let _g2 = m2.lock().unwrap(); thread::sleep(std::time::Duration::from_millis(10)); let _g1 = m1.lock().unwrap(); }); handle1.join().unwrap(); handle2.join().unwrap(); }Implemente um contador atômico usando
AtomicUsizeem vez deMutex. Compare a performance com a versão comMutex.✓ Resposta: Versão comAtomicUsize:
A versão atômica é geralmente mais rápida que a com Mutex, pois não envolve bloqueio de thread.use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::thread; fn main() { let counter = Arc::new(AtomicUsize::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { for _ in 0..1000 { counter.fetch_add(1, Ordering::SeqCst); } }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", counter.load(Ordering::SeqCst)); }AtomicUsizeé ideal para contadores simples.