Concurrence en Rust : des mutex à async/await
Quand il s'agit de systèmes haute performance, Rust se retrouve instantanément sous les projecteurs. Mais qu'est-ce qui le rend vraiment unique ? C'est la concurrence en Rust — une approche où vous obtenez la vitesse du C et la sécurité mémoire, sans sacrifier la lisibilité du code. Dans cet article, nous allons décortiquer trois outils principaux : les threads classiques, les goroutines légères (dans le contexte de Rust — les tâches asynchrones) et le moderne async/await. C'est parti !
Pourquoi Rust n'a-t-il pas peur de la concurrence ?
Contrairement à de nombreux langages, Rust résout le problème des conditions de course au niveau du compilateur. Le système de propriété (ownership) et d'emprunt (borrowing) garantit que vous ne pouvez pas modifier accidentellement des données depuis deux threads simultanément. Ce n'est pas de la magie — c'est une vérification statique stricte. C'est pourquoi le multithreading en Rust est considéré comme l'un des plus sûrs.
1. Threads (std::thread)
Les threads OS classiques sont la base. En Rust, ils sont implémentés via le module std::thread. Chaque thread est un processus léger distinct qui peut s'exécuter en parallèle sur différents cœurs CPU.
Exemple : lancement de threads
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();}Ici, thread::spawn crée un nouveau thread, et join attend sa fin. Simple, mais il y a un inconvénient : les threads sont « lourds ». Chaque thread consomme au moins 2-8 Mo de mémoire de pile. Pour des milliers de tâches simultanées, ce n'est pas adapté.
Quand utiliser les threads ?
- Quand un vrai parallélisme est nécessaire (tâches CPU-bound).
- Quand le nombre de threads ne dépasse pas le nombre de cœurs * 2.
- Quand vous travaillez avec des opérations bloquantes (lecture de fichiers, appels synchrones).
2. Goroutines en Rust : mythe ou réalité ?
À proprement parler, Rust n'a pas de goroutines au sens où Go en a. Mais il existe des tâches asynchrones (tasks) qui se comportent presque de la même manière : elles sont légères, commutent de manière coopérative (sans intervention de l'OS) et peuvent être lancées par milliers sur un seul thread. Pour cela, on utilise le runtime Tokio ou async-std.
Exemple : tâche asynchrone (goroutine) via Tokio
use tokio::task;
#[tokio::main]async fn main() { let handle = task::spawn(async { for i in 1..10 { println!("Goroutine : {}", i); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; } });
for i in 1..5 { println!("Principal : {}", i); tokio::time::sleep(tokio::time::Duration::from_millis(1)).await; }
handle.await.unwrap();}Notez que task::spawn crée une tâche légère qui s'exécute dans le pool de threads de Tokio. Le coût mémoire est d'environ 2-3 Ko par tâche. Cela permet de lancer 100 000 « goroutines » sans problème. Contrairement à Go, il n'y a pas d'ordonnanceur intégré — vous choisissez vous-même le runtime.
En quoi les goroutines Rust diffèrent-elles de Go ?
- En Rust — les tâches sont coopératives : elles cèdent le contrôle via
.await. - En Go — les goroutines sont préemptives : l'ordonnanceur les interrompt aux points d'entrée-sortie.
- Rust offre plus de contrôle : vous pouvez choisir un runtime mono-thread ou multi-thread.
3. Async/await : le cœur du Rust asynchrone
Les mots-clés async et .await transforment le code synchrone en code asynchrone sans s