Concurrence et multithreading en Rust : écrire du code parallèle sécurisé
Rust est l'un des rares langages qui résout le problème des courses de données (data races) au moment de la compilation. Grâce au système de possession (ownership) et d'emprunt (borrowing), le code concurrent en Rust devient non seulement rapide, mais aussi sécurisé. Dans cet article, nous allons aborder les bases du multithreading en Rust : création de threads, échange de données via des canaux et des mutex, ainsi que la découverte du modèle « Fearless Concurrency ».
1. Bases du multithreading : std::thread
Rust offre un support intégré pour les threads via le module std::thread. Créer un nouveau thread est possible avec la fonction thread::spawn, qui prend une fermeture (closure).
use std::thread;use std::time::Duration;
fn main() { let handle = thread::spawn(|| { for i in 1..10 { println!("Thread : {}", i); thread::sleep(Duration::from_millis(1)); } });
for i in 1..5 { println!("Thread principal : {}", i); thread::sleep(Duration::from_millis(1)); }
handle.join().unwrap(); // attend la fin du thread}La méthode join() bloque le thread principal jusqu'à ce que le thread enfant se termine. Sans elle, le programme pourrait se terminer avant que le thread ne s'exécute.
2. Transfert de données entre threads : canaux (mpsc)
Pour échanger des messages entre threads, Rust utilise le modèle de canal multi-producteur, mono-consommateur (mpsc — multiple producer, single consumer). Cela est implémenté dans le module std::sync::mpsc.
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
thread::spawn(move || { let val = String::from("Bonjour depuis le thread !"); tx.send(val).unwrap(); // val a déjà été déplacé, on ne peut plus l'utiliser });
let received = rx.recv().unwrap(); println!("Reçu : {}", received);}Notez le mot-clé move dans la fermeture — il transfère la possession de la variable tx au nouveau thread. La méthode send() retourne un Result, qui peut être Err si le récepteur a été fermé.
Pour créer plusieurs producteurs, on peut cloner tx :
let (tx, rx) = mpsc::channel();let tx1 = tx.clone();
thread::spawn(move || { tx.send("Message 1").unwrap();});
thread::spawn(move || { tx1.send("Message 2").unwrap();});
for received in rx { println!("Reçu : {}", received);}3. Mémoire partagée : Mutex et Arc
Les canaux sont pratiques pour transférer des données, mais parfois il est nécessaire de partager un état entre threads. Pour cela, on utilise Mutex (exclusion mutuelle) en combinaison avec Arc (compteur de références atomique).
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());}Pourquoi Arc et non Rc ? Le Rc classique n'est pas thread-safe, car son compteur de références n'est pas atomique. Arc utilise des opérations atomiques, ce qui le rend sûr pour le multithreading.
Important : Mutex bloque le thread lors de l'appel à lock(). Après la sortie de la portée, le verrou est automatiquement libéré (grâce au trait Drop).