Mutex
Quando più thread devono accedere e modificare gli stessi dati, è necessario un meccanismo di sincronizzazione. In Rust, Mutex<T> (mutual exclusion) garantisce che un solo thread alla volta possa accedere al dato protetto, prevenendo data race a tempo di compilazione.
Creare e Usare un Mutex
Un Mutex<T> avvolge un valore di tipo T e ne controlla l’accesso tramite il metodo lock():
use std::sync::Mutex;
fn main() {
let m = Mutex::new(5);
{
let mut valore = m.lock().unwrap();
*valore = 10;
println!("Valore dentro il lock: {}", *valore);
} // MutexGuard viene droppato qui, rilasciando il lock
println!("Mutex contiene: {:?}", m);
}
MutexGuard e RAII
lock() restituisce un MutexGuard<T>, uno smart pointer che implementa Deref e DerefMut. Il lock viene rilasciato automaticamente quando il MutexGuard esce dallo scope (pattern RAII):
use std::sync::Mutex;
fn main() {
let dati = Mutex::new(vec![1, 2, 3]);
{
let mut guard = dati.lock().unwrap();
guard.push(4);
guard.push(5);
// Il lock è attivo qui
} // lock rilasciato automaticamente
// Ora altri possono accedere ai dati
let guard = dati.lock().unwrap();
println!("Dati: {:?}", *guard);
}
Poisoning del Mutex
Se un thread va in panic mentre detiene un lock, il Mutex viene considerato poisoned (avvelenato). I successivi tentativi di acquisire il lock restituiranno un errore:
use std::sync::Mutex;
use std::thread;
fn main() {
let mutex = Mutex::new(0);
let risultato = thread::spawn(|| {
let _guard = mutex.lock().unwrap();
panic!("Qualcosa è andato storto!");
}).join();
// Il mutex è ora poisoned
match mutex.lock() {
Ok(guard) => println!("Valore: {}", *guard),
Err(poisoned) => {
// Possiamo comunque recuperare i dati
let guard = poisoned.into_inner();
println!("Mutex avvelenato, valore recuperato: {}", *guard);
}
}
}
Mutex con Arc per Condivisione tra Thread
Per condividere un Mutex tra più thread, lo si avvolge in un Arc (Atomic Reference Counted):
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let contatore = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let contatore = Arc::clone(&contatore);
let handle = thread::spawn(move || {
let mut num = contatore.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Contatore finale: {}", *contatore.lock().unwrap());
}
Arc permette ownership condivisa thread-safe, mentre Mutex garantisce accesso esclusivo. Insieme formano il pattern piu comune per lo stato condiviso in Rust.
RwLock: Lettori Multipli, Scrittore Singolo
RwLock<T> è un’alternativa a Mutex che permette accessi simultanei in lettura, ma richiede accesso esclusivo per la scrittura:
use std::sync::{Arc, RwLock};
use std::thread;
fn main() {
let dati = Arc::new(RwLock::new(vec![1, 2, 3]));
let mut handles = vec![];
// Più lettori simultanei
for i in 0..3 {
let dati = Arc::clone(&dati);
handles.push(thread::spawn(move || {
let lettura = dati.read().unwrap();
println!("Lettore {}: {:?}", i, *lettura);
}));
}
// Un singolo scrittore (attende che i lettori finiscano)
{
let dati = Arc::clone(&dati);
handles.push(thread::spawn(move || {
let mut scrittura = dati.write().unwrap();
scrittura.push(4);
println!("Scrittore: aggiunto elemento");
}));
}
for handle in handles {
handle.join().unwrap();
}
println!("Dati finali: {:?}", *dati.read().unwrap());
}
Usa RwLock quando le letture sono molto più frequenti delle scritture, poiché permette maggiore parallelismo.
Prevenzione dei Deadlock
Un deadlock si verifica quando due o più thread si bloccano reciprocamente in attesa di risorse. Ecco un esempio di deadlock e come evitarlo:
use std::sync::{Arc, Mutex};
// ESEMPIO DI DEADLOCK (da evitare!)
// Thread 1: lock(a) -> lock(b)
// Thread 2: lock(b) -> lock(a)
// SOLUZIONE: acquisire i lock sempre nello stesso ordine
fn trasferisci(
conto_a: &Mutex<f64>,
conto_b: &Mutex<f64>,
importo: f64,
) {
// Acquisire sempre nello stesso ordine previene il deadlock
let mut a = conto_a.lock().unwrap();
let mut b = conto_b.lock().unwrap();
*a -= importo;
*b += importo;
}
fn main() {
let conto1 = Arc::new(Mutex::new(1000.0));
let conto2 = Arc::new(Mutex::new(500.0));
let c1 = Arc::clone(&conto1);
let c2 = Arc::clone(&conto2);
thread::scope(|s| {
s.spawn(|| trasferisci(&c1, &c2, 100.0));
s.spawn(|| trasferisci(&c1, &c2, 50.0));
});
println!("Conto 1: {}", conto1.lock().unwrap());
println!("Conto 2: {}", conto2.lock().unwrap());
}
Regole per prevenire i deadlock:
- Acquisire i lock sempre nello stesso ordine
- Ridurre al minimo il tempo di possesso del lock
- Evitare di acquisire più lock contemporaneamente quando possibile
- Usare
try_lock()per tentativi non bloccanti
use std::sync::Mutex;
fn main() {
let risorsa = Mutex::new(42);
// try_lock() non si blocca
match risorsa.try_lock() {
Ok(guard) => println!("Lock acquisito: {}", *guard),
Err(_) => println!("Lock non disponibile, riprovo dopo"),
}
}
Conclusione
Mutex<T> e RwLock<T> sono gli strumenti principali per la condivisione sicura dello stato tra thread in Rust. Combinati con Arc<T> per la proprietà condivisa, permettono di costruire strutture dati thread-safe. Ricorda: usa Mutex quando hai bisogno di accesso esclusivo, RwLock quando le letture dominano, e segui sempre un ordine coerente nell’acquisizione dei lock per prevenire deadlock.