Concurrence et multithreading en Rust : des bases aux techniques avancées
Rust a acquis la réputation d'être l'un des langages de programmation système les plus sûrs, en grande partie grâce à son modèle de possession (ownership) et à son système de types qui empêche les courses de données à la compilation. Cependant, la concurrence ne se limite pas à la sécurité, elle concerne aussi l'efficacité. Dans cet article, nous examinerons les aspects avancés du travail avec la concurrence et le multithreading en Rust : des primitives de synchronisation de base aux runtimes asynchrones modernes. Ce contenu est destiné aux développeurs déjà familiers avec les bases du langage.
1. Multithreading sécurisé : Send, Sync et Arc<Mutex<T>>
La base du multithreading sécurisé en Rust repose sur deux traits clés : Send et Sync. Un type implémentant Send peut être transféré en toute sécurité entre les threads. Un type implémentant Sync peut être partagé en toute sécurité entre les threads par référence. La plupart des types standards (par exemple, i32, String) implémentent automatiquement les deux traits s'ils ne contiennent que des données Send/Sync.
Pour l'accès partagé à des données mutables depuis plusieurs threads, on utilise souvent le motif Arc<Mutex<T>>. Arc (compteur de références atomique) permet de partager la possession entre les threads, tandis que Mutex assure l'exclusion mutuelle.
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());}Il est important de se rappeler : Mutex en Rust n'est pas simplement un verrou, mais un conteneur qui protège les données. L'acquisition du verrou retourne un pointeur intelligent MutexGuard, qui libère automatiquement le verrou lorsqu'il sort de la portée. Cela élimine l'erreur classique "j'ai oublié de déverrouiller".
Conseil avancé : RwLock au lieu de Mutex
Si vos données sont souvent lues mais rarement modifiées, utilisez RwLock. Il permet à plusieurs threads de lire les données simultanément, mais bloque l'écriture tant qu'il y a au moins un lecteur. Cela peut considérablement améliorer les performances dans les scénarios où "il y a plus de lecteurs que d'écrivains".
use std::sync::RwLock;
let data = RwLock::new(vec![1, 2, 3]);
// Lecture — plusieurs threads peuvent lire simultanémentlet read_guard = data.read().unwrap();println!("First element: {}", read_guard[0]);drop(read_guard); // Libération explicite du verrou
// Écriture — accès exclusiflet mut write_guard = data.write().unwrap();write_guard.push(4);2. Canaux (Channels) : transmission de messages entre threads
Les canaux sont une alternative à la mémoire partagée. En Rust, il existe deux principaux types de canaux : std::sync::mpsc (Multi-Producer, Single-Consumer) et crossbeam-channel (Multi-Producer, Multi-Consumer).
Le mpsc standard est parfait pour les scénarios "un consommateur, plusieurs producteurs". Il fonctionne sur le principe d'une file d'attente : les expéditeurs déposent des messages, le destinataire les récupère dans l'ordre FIFO.
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
let tx1 = tx.clone(); thread::spawn(move || { tx1.send("Message du thread 1").unwrap(); });
thread::spawn(move || { tx.send("Message du thread 2").unwrap(); });
for received in rx { println!("Reçu : {}", received); }}