Concurrence et multithreading en Rust : écrire du code sûr et rapide
Les programmes modernes fonctionnent rarement en mode monothread. Le traitement des requêtes, le travail avec les fichiers, les opérations réseau — tout cela nécessite une exécution parallèle des tâches. Rust propose une approche unique de la concurrence : il garantit la sécurité mémoire au moment de la compilation, empêchant les courses de données. Dans cet article, nous allons voir comment travailler avec les threads, les canaux, les mutex et l'asynchronie en Rust.
1. Bases du multithreading : création et gestion des threads
En Rust, les threads sont créés via la bibliothèque standard std::thread. Chaque thread s'exécute indépendamment, mais le compilateur surveille strictement la durée de vie des variables.
Création d'un thread
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(100)); } });
for i in 1..5 { println!("Thread principal : {}", i); thread::sleep(Duration::from_millis(100)); }
handle.join().unwrap(); // Attend la fin du thread}Capture de la propriété dans les closures
Si un thread utilise des variables de la portée externe, il faut transférer la propriété via move :
let data = vec![1, 2, 3];
let handle = thread::spawn(move || { println!("Données : {:?}", data);});
handle.join().unwrap();// println!("{:?}", data); // Erreur ! data a été déplacée2. Échange de données : mutex et verrous
Pour un accès sécurisé aux données partagées depuis plusieurs threads, Rust utilise Mutex (mutex) en combinaison avec Arc (compteur de références atomique).
Exemple avec Mutex et Arc
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()); // 10}Important : Mutex se déverrouille automatiquement en sortant de la portée. Cela empêche le deadlock (interblocage) au niveau de la compilation.
RwLock — pour des lectures fréquentes
Si les données sont rarement modifiées mais souvent lues, utilisez RwLock :
use std::sync::RwLock;
let lock = RwLock::new(5);
{ let r1 = lock.read().unwrap(); let r2 = lock.read().unwrap(); // Lecture simultanée possible println!("{} {}", r1, r2);}
{ let mut w = lock.write().unwrap(); *w += 1; // L'écriture bloque la lecture}3. Canaux : transmission de messages entre threads
Rust prend en charge le modèle des acteurs via les canaux (std::sync::mpsc). Cela permet aux threads d'échanger des données sans mémoire partagée.
Exemple avec un canal
use std::sync::mpsc;use std::thread;
fn main() { let (tx, rx) = mpsc::channel();
thread::spawn(move || { let vals = vec!["hello", "from", "the", "thread"]; for val in vals { tx.send(val).unwrap(); thread::sleep(std::time::Duration::from_secs(1)); } });
for received in rx { println!("Reçu : {}", received); }}Plusieurs expéditeurs
Avec mpsc::Sender, on peut cloner l'expéditeur pour plusieurs threads :
let (tx, rx) = mpsc::channel();let tx1 = tx.clone();
thread::spawn(move || { tx1.send("Thread 1").unwrap(); });thread::spawn(move || { tx.send("Thread 2").unwrap(); });
for received in rx { println!("Reçu : {}", received);}
```