ProgrammationDéveloppeur Backend

Quels sont les moyens de synchronisation des threads fournis par la bibliothèque standard de Rust ? Comment choisir entre eux et quels sont les « points délicats » à prendre en compte ?

Réussissez les entretiens avec l'assistant IA Hintsage

Réponse

La bibliothèque standard de Rust propose des primitives de synchronisation fondamentales pour une utilisation sûre de la concurrence :

  • Mutex — fournit une exclusion mutuelle pour l'accès aux données depuis plusieurs threads ;
  • RwLock — permet plusieurs lecteurs (read) simultanément, mais un seul écrivain (write) ;
  • Condvar — primitive de variable conditionnelle pour organiser le réveil des threads en fonction d'un événement ;
  • Types atomiques (AtomicBool, AtomicUsize, etc.) — opérations de lecture/écriture sans verrouillage, au niveau matériel ;
  • Arc (Atomic Reference Counted) — comptage de références avec sécurité des threads, pour la possession partagée d'objets.

Choix :

  • Si seule la lecture est requise simultanément — utilisez RwLock (plus efficace que Mutex).
  • Pour un accès synchronisé simple d'un thread — Mutex.
  • Pour la synchronisation basée sur un signal (par exemple, « attendre un nouvel élément ») — Condvar.
  • Pour des compteurs/fiches atomiques — Atomic.
  • Pour la possession partagée (multithreading) — Arc.

Exemple :

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

Question piège

Question : Rust garantit-il que l'utilisation de Mutex<T> élimine complètement les deadlocks grâce à une vérification lors de la compilation ?

Réponse : Non. Rust assure un accès sûr aux données via la propriété et le borrow-checker, mais ne protège pas des deadlocks au niveau du langage. Les blocages morts surviennent logiquement en cas de violation de l'ordre de capture de plusieurs Mutex ou de leur capture récursive. Exemple :

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

Histoire

Un projet de traitement de données a rencontré des "accrocs" aléatoires. Il s'est avéré que les développeurs utilisaient des Mutex imbriqués (Mutex à l'intérieur d'un Mutex) sans un ordre strict de capture, ce qui entraînait des blocages mutuels (deadlock), ne pouvant être résolus que par un arrêt forcé du processus.

Histoire

Dans un grand service, ils ont massivement migré de Mutex<Option<T>> à RwLock<T>, sans tenir compte du fait que le verrou d'écriture pouvait être plus long qu'un verrou de lecture. Pendant les pics de charge, cela a entraîné des délais de traitement allant jusqu'à des dizaines de secondes en raison des files d'attente pour écrire.

Histoire

Les programmeurs ont essayé d'économiser sur les threads, "poussant" Arc<Mutex<_>> dans des centaines de threads. En raison des subtilités du fonctionnement du planificateur et du réutilisation des mutex, des attentes mutuelles surprenantes sont apparues - la performance a chuté de 5 fois !