00
:
00
:
00
:
00
•Corso SEO AI - Usa SEOEMAIL al checkout per il 30% di sconto

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.