Concurrence et multithreading en Rust : techniques avancées

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Concurrence et multithreading en Rust : niveau avancé

\n\n

La concurrence et le multithreading sont parmi les aspects les plus puissants et en même temps les plus complexes de la programmation système. Rust propose une approche unique : des garanties de sécurité mémoire à la compilation, éliminant les conditions de course (data race) et les pointeurs pendants. Dans cet article, nous aborderons les techniques avancées de travail avec la concurrence en Rust, notamment async/await, la bibliothèque Tokio, les traits Send et Sync, ainsi que les primitives de synchronisation de bas niveau.

\n\n

Ce contenu est destiné aux développeurs déjà familiers avec les bases de Rust et souhaitant approfondir le calcul parallèle haute performance.

\n\n

1. Modèles de concurrence : threads OS et asynchrone

\n\n

Rust prend en charge deux modèles principaux : les threads du système d'exploitation (OS threads) et les tâches asynchrones (async tasks). Le choix entre eux dépend du scénario : pour les tâches intensives en CPU, les threads sont plus adaptés ; pour les tâches liées aux E/S, l'async est préférable.

\n\n

Threads OS (std::thread)

\n\n

La création d'un thread en Rust est triviale, mais nécessite une attention particulière à la propriété des données :

\n\n
use std::thread;\n\nlet data = vec![1, 2, 3];\n\nlet handle = thread::spawn(move || {\n    println!("{:?}", data);\n});\n\nhandle.join().unwrap();
\n\n

Le point clé est le mot-clé move. Il transfère la propriété à la fermeture. Si vous devez partager des données entre threads, utilisez Arc<Mutex<T>>.

\n\n

Programmation asynchrone (async/await)

\n\n

Le code asynchrone s'exécute dans un seul thread, en basculant entre les tâches pendant l'attente d'E/S. Cela réduit les frais généraux de changement de contexte. Exemple d'une fonction asynchrone simple :

\n\n
use tokio::time::{sleep, Duration};\n\n#[tokio::main]\nasync fn main() {\n    let task1 = async_task("Tâche 1");\n    let task2 = async_task("Tâche 2");\n\n    tokio::join!(task1, task2);\n}\n\nasync fn async_task(name: &str) {\n    println!("{} a commencé", name);\n    sleep(Duration::from_secs(1)).await;\n    println!("{} est terminée", name);\n}
\n\n

La bibliothèque tokio est la norme pour le Rust asynchrone. Elle fournit un planificateur multithread (work-stealing scheduler) qui répartit efficacement les tâches entre les cœurs du CPU.

\n\n

2. Traits Send et Sync : sécurité de la concurrence

\n\n

Deux traits clés assurent la sécurité de la concurrence en Rust :

\n\n
    \n
  • Send — un type peut être transféré en toute sécurité entre les threads.
  • \n
  • Sync — un type peut être référencé en toute sécurité depuis plusieurs threads simultanément.
  • \n
\n\n

La plupart des primitives Rust implémentent automatiquement ces traits si leurs composants les implémentent. Par exemple, Arc<Mutex<i32>> est Send et Sync, alors que Rc<RefCell<i32>> ne l'est pas.

\n\n

Considérons un exemple qui ne compilera pas en raison d'une violation de Send :

\n\n
use std::rc::Rc;\nuse std::thread;\n\nlet data = Rc::new(5);\n\nthread::spawn(move || {\n    println!("{}", data);\n}); // Erreur : Rc<i32> n'implémente pas Send
\n\n

Le compilateur Rust émettra un message d'erreur clair, empêchant les conditions de course à la compilation. C'est l'un des principaux avantages du langage.

\n\n

3. Synchronisation avancée : canaux, mutex et RwLock

\n\n

Pour échanger des données entre threads, Rust propose plusieurs primitives. Examinons les plus puissantes.

\n\n

Canaux (mpsc)

\n\n

Les canaux multithread (multi-producteur, mono-consommateur) permettent d'envoyer des messages depuis plusieurs threads vers un seul récepteur :

\n\n
use std::sync::mpsc;\nuse std::thread;\n\nlet (tx, rx) = mpsc::channel();\n\nfor i in 0..3 {\n    let tx_clone = tx.clone();\n    thread::spawn(move || {\n        tx_clone.send(i).unwrap();\n    });\n}\n\ndrop(tx); // Fermeture de l'émetteur\n\nfor received in rx {\n    println!("Reçu : {}", received);\n}
\n\n

Les canaux sont idéaux pour l'architecture "producteur

Blogs

Book Recommendations