Arc e Mutex
Esta aula aborda o compartilhamento seguro de dados entre threads em Rust usando Arc e Mutex, explicando as diferenças entre Arc e Rc, o mecanismo de Mutex e o fenômeno de poisoning, além de como evitar deadlocks.
Em Rust, o sistema de ownership e borrowing garante segurança de memória em tempo de compilação, mas quando trabalhamos com concorrência, precisamos de mecanismos adicionais para compartilhar dados entre threads de forma segura. Arc (Atomic Reference Counting) e Mutex (Mutual Exclusion) são duas ferramentas fundamentais para esse fim. Nesta aula, vamos explorar como usá-los corretamente, entender suas diferenças com Rc, lidar com poisoning e evitar deadlocks.
Compartilhamento entre threads
Para compartilhar dados entre threads, precisamos que o tipo implemente a trait Send (pode ser transferido entre threads) e Sync (pode ser referenciado por múltiplas threads simultaneamente). Arc fornece uma referência contada atômica que permite que múltiplas threads possuam acesso compartilhado ao mesmo dado, desde que o tipo interno seja Sync. Mutex, por sua vez, garante que apenas uma thread por vez acesse o dado, protegendo contra condições de corrida.
Um padrão comum é combinar Arc e Mutex: Arc<Mutex<T>>. O Arc permite que várias threads mantenham uma referência ao Mutex, e o Mutex serializa o acesso ao valor interno.
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
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());
}Neste exemplo, 10 threads incrementam um contador compartilhado. O Arc::clone incrementa a contagem de referências, e lock() retorna um guard que desbloqueia automaticamente ao sair de escopo.
Arc vs Rc
Rc (Reference Counting) é usado para compartilhamento dentro de uma única thread, pois não é Send nem Sync. Arc é a versão atômica de Rc, adequada para ambientes concorrentes. A diferença principal é que Arc usa operações atômicas para gerenciar a contagem de referências, o que tem um custo de desempenho maior, mas é necessário para segurança entre threads. Rc é mais leve e deve ser preferido quando não há concorrência.
Exemplo: se você tentar usar Rc em múltiplas threads, o compilador rejeitará:
use std::rc::Rc;
use std::thread;
fn main() {
let data = Rc::new(42);
thread::spawn(move || {
println!("{}", data); // erro: Rc<i32> não implementa Send
});
}Para corrigir, troque Rc por Arc:
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(42);
let data_clone = Arc::clone(&data);
thread::spawn(move || {
println!("{}", data_clone);
});
}Mutex e poisoning
Um Mutex em Rust pode estar em estado "poisoned" (envenenado) quando uma thread entra em pânico enquanto detém o lock. Quando isso acontece, o Mutex marca seu estado como poisoned para evitar que outras threads acessem dados potencialmente corrompidos. Tentar chamar lock() em um Mutex poisoned retorna um Err (especificamente PoisonError).
Para lidar com isso, podemos usar lock().unwrap_or_else() ou ignorar o erro com lock().unwrap() (que também causaria pânico). A melhor prática é tratar o erro adequadamente, por exemplo, resetando o valor ou propagando o erro.
use std::sync::{Mutex, PoisonError};
fn main() {
let mutex = Mutex::new(0);
// Simulando um pânico enquanto segura o lock
let handle = std::thread::spawn(move || {
let _guard = mutex.lock().unwrap();
panic!("Oops!");
});
// mutex foi movido para a thread, então não podemos mais usar aqui
// Para demonstrar, criaremos um novo mutex
let mutex2 = Mutex::new(10);
let result = std::panic::catch_unwind(|| {
let _guard = mutex2.lock().unwrap();
panic!("Panic inside");
});
// mutex2 está poisoned
match mutex2.lock() {
Ok(mut val) => *val += 1,
Err(e) => {
println!("Mutex poisoned, recovering...");
let mut val = e.into_inner();
*val = 0; // reset
}
}
println!("Final value: {}", *mutex2.lock().unwrap());
}No código acima, capturamos o pânico e tratamos o erro de poisoning, recuperando o lock interno com into_inner().
Deadlocks
Deadlock ocorre quando duas ou mais threads ficam bloqueadas esperando por locks que nunca serão liberados, geralmente devido a uma ordem inconsistente de aquisição. Em Rust, deadlocks não são evitados pelo compilador; cabe ao programador projetar o código para evitá-los.
Uma estratégia comum é sempre adquirir os locks na mesma ordem em todas as threads. Outra é usar try_lock() que não bloqueia, permitindo que a thread desista se não conseguir o lock imediatamente.
use std::sync::{Mutex, Arc};
use std::thread;
fn main() {
let resource_a = Arc::new(Mutex::new(0));
let resource_b = Arc::new(Mutex::new(0));
let a1 = Arc::clone(&resource_a);
let b1 = Arc::clone(&resource_b);
let handle1 = thread::spawn(move || {
let _lock_a = a1.lock().unwrap();
thread::sleep(std::time::Duration::from_millis(10));
let _lock_b = b1.lock().unwrap(); // pode causar deadlock se outra thread fizer o oposto
});
let a2 = Arc::clone(&resource_a);
let b2 = Arc::clone(&resource_b);
let handle2 = thread::spawn(move || {
let _lock_b = b2.lock().unwrap();
thread::sleep(std::time::Duration::from_millis(10));
let _lock_a = a2.lock().unwrap(); // ordem inversa -> deadlock
});
handle1.join().unwrap();
handle2.join().unwrap();
}Para evitar o deadlock, garantir que ambas as threads adquiram os locks na mesma ordem (por exemplo, sempre A antes de B). Alternativamente, usar try_lock com backoff.
Boas práticas
- Prefira
Arc<Mutex<T>>apenas quando necessário; para dados imutáveis, useArc<T>diretamente. - Mantenha o escopo do lock o menor possível para evitar contenção e deadlocks.
- Evite chamar funções desconhecidas ou que possam causar pânico enquanto segura um lock.
- Considere usar
RwLockpara cenários de leitura frequente e escrita esporádica. - Use ferramentas como
parking_lot::Mutexpara melhor desempenho em alguns casos, mas entenda as diferenças.
Referências
- Arc - Rust Standard Library
- Mutex - Rust Standard Library
- The Rust Programming Language: Shared State
- PoisonError - Rust Standard Library
- The Rustonomicon: Send and Sync
- Rc - Rust Standard Library
Exercícios
Escreva um programa que cria 5 threads, cada uma incrementando um contador compartilhado (usando Arc e Mutex) 1000 vezes. Ao final, imprima o valor do contador.
✓ Resposta:use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..5 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { for _ in 0..1000 { let mut num = counter.lock().unwrap(); *num += 1; } }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Counter: {}", *counter.lock().unwrap()); }Explique por que o código abaixo não compila e como corrigi-lo:
use std::rc::Rc; use std::thread; fn main() { let data = Rc::new(42); let handle = thread::spawn(move || { println!("{}", data); }); handle.join().unwrap(); }✓ Resposta: O erro é queRc<i32>não implementaSend, portanto não pode ser transferido para outra thread. A correção é usarArcno lugar deRc. Além disso, é necessário clonar o Arc para cada thread. Código corrigido:use std::sync::Arc; use std::thread; fn main() { let data = Arc::new(42); let data_clone = Arc::clone(&data); let handle = thread::spawn(move || { println!("{}", data_clone); }); handle.join().unwrap(); }Crie um cenário onde um Mutex fica poisoned e implemente uma recuperação que resete o valor para 0.
✓ Resposta:use std::sync::{Mutex, PoisonError}; fn main() { let mutex = Mutex::new(10); let result = std::panic::catch_unwind(|| { let _guard = mutex.lock().unwrap(); panic!("Panic!"); }); // mutex está poisoned match mutex.lock() { Ok(mut val) => println!("Value: {}", *val), Err(e) => { let mut val = e.into_inner(); *val = 0; println!("Reset to 0"); } } println!("Final: {}", *mutex.lock().unwrap()); }Escreva um código que cause um deadlock entre duas threads usando dois Mutexes e depois corrija-o garantindo ordem consistente.
✓ Resposta: Código com deadlock:
Correção: ambas as threads adquirem os locks na mesma ordem (por exemplo, a antes de b):use std::sync::{Arc, Mutex}; use std::thread; fn main() { let a = Arc::new(Mutex::new(0)); let b = Arc::new(Mutex::new(0)); let a1 = Arc::clone(&a); let b1 = Arc::clone(&b); let h1 = thread::spawn(move || { let _la = a1.lock().unwrap(); thread::sleep(std::time::Duration::from_millis(10)); let _lb = b1.lock().unwrap(); }); let a2 = Arc::clone(&a); let b2 = Arc::clone(&b); let h2 = thread::spawn(move || { let _lb = b2.lock().unwrap(); thread::sleep(std::time::Duration::from_millis(10)); let _la = a2.lock().unwrap(); }); h1.join().unwrap(); h2.join().unwrap(); }// ... mesma configuração, mas h2: let h2 = thread::spawn(move || { let _la = a2.lock().unwrap(); thread::sleep(std::time::Duration::from_millis(10)); let _lb = b2.lock().unwrap(); });Usando
try_lock, implemente um mecanismo que evite deadlock: cada thread tenta adquirir ambos os locks, e se falhar, libera o lock já obtido e tenta novamente após um curto período.✓ Resposta:use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; fn main() { let a = Arc::new(Mutex::new(0)); let b = Arc::new(Mutex::new(0)); let a1 = Arc::clone(&a); let b1 = Arc::clone(&b); let h1 = thread::spawn(move || loop { if let Ok(la) = a1.try_lock() { if let Ok(lb) = b1.try_lock() { // critical section break; } else { drop(la); thread::sleep(Duration::from_millis(1)); } } else { thread::sleep(Duration::from_millis(1)); } }); let a2 = Arc::clone(&a); let b2 = Arc::clone(&b); let h2 = thread::spawn(move || loop { if let Ok(lb) = b2.try_lock() { if let Ok(la) = a2.try_lock() { // critical section break; } else { drop(lb); thread::sleep(Duration::from_millis(1)); } } else { thread::sleep(Duration::from_millis(1)); } }); h1.join().unwrap(); h2.join().unwrap(); }