ProgrammazioneSviluppatore Backend

Quali sono i modi di sincronizzazione dei thread forniti dalla libreria standard di Rust? Come scegliere tra di essi e quali "aspetti critici" è necessario considerare?

Supera i colloqui con l'assistente IA Hintsage

Risposta

Nella libreria standard di Rust sono presenti i principali primitivi di sincronizzazione per un uso sicuro della multithreading:

  • Mutex — garantisce l'esclusione mutua per l'accesso ai dati da più thread;
  • RwLock — consente a più lettori (read) di accedere simultaneamente, ma solo a uno scrittore (write);
  • Condvar — primitivo di variabile condizionale per organizzare il risveglio dei thread in base a un evento;
  • Tipi Atomic (AtomicBool, AtomicUsize e altri) — operazioni di lettura/scrittura senza lock, a livello hardware;
  • Arc (Atomic Reference Counted) — conteggio dei riferimenti con sicurezza nei thread, per la condivisione di oggetti.

Scelta:

  • Se è richiesta solo la lettura contemporaneamente — utilizzare RwLock (più efficace di Mutex).
  • Per l'accesso sincronizzato di un singolo thread — Mutex.
  • Per la sincronizzazione tramite segnale (ad esempio, "attendere un nuovo elemento") — Condvar.
  • Per contatori/flag atomici — Atomic.
  • Per proprietà condivisa (multithreading) — Arc.

Esempio:

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!("Risultato: {}", *counter.lock().unwrap()); }

Domanda insidiosa

Domanda: Garantiscono i Rust che l'uso di Mutex<T> elimina completamente i deadlock grazie al controllo in fase di compilazione?

Risposta: No. Rust garantisce un accesso sicuro ai dati tramite possesso e borrow-checker, ma non protegge dai deadlock a livello di linguaggio. I deadlock si verificano puramente logicamente quando si viola l'ordine di acquisizione di più Mutex o si acquisisce in modo ricorsivo. Esempio:

use std::sync::Mutex; let lock1 = Mutex::new(0); let lock2 = Mutex::new(0); // Thread 1: lock1 -> lock2, Thread 2: lock2 -> lock1 ⇒ deadlock

Storia

Il progetto di elaborazione dei dati ha subito "bloccaggi" casuali. Si è scoperto che gli sviluppatori utilizzavano Mutex nidificati (Mutex dentro Mutex) senza un rigoroso ordine di acquisizione, il che portava a deadlock, risolvibili solo tramite la terminazione forzata del processo.

Storia

In un grande servizio si è migrato massicciamente da Mutex<Option<T>> a RwLock<T>, senza considerare che il lock in scrittura potrebbe richiedere più tempo del lock in lettura. Nei picchi di carico, questo ha causato ritardi di elaborazione fino a decine di secondi a causa delle code per la scrittura.

Storia

I programmatori cercavano di risparmiare sui thread, "spingendo" Arc<Mutex<_>> in centinaia di thread. A causa delle sfumature del funzionamento dello scheduler e del riutilizzo dei mutex, si verificavano sorprendenti attese reciproche — le prestazioni sono diminuite di 5 volte!