Introduction : pourquoi des files d'attente de tâches en Rust ?
\n\nDans le développement moderne de systèmes à forte charge, les files d'attente de tâches (task queues) constituent un modèle fondamental. Elles permettent de séparer la génération des tâches de leur exécution, d'équilibrer la charge, de faire évoluer les workers et de construire des applications tolérantes aux pannes. Rust, avec son contrôle strict de la mémoire et l'absence de ramasse-miettes, offre des possibilités uniques pour implémenter des files d'attente efficaces.
\n\nDans cet article, nous allons examiner trois approches pratiques pour travailler avec les files d'attente de tâches en Rust :
\n- \n
- Utilisation des canaux standard
mpsc(multi-producteur, mono-consommateur) ; \n - Files d'attente multithread avec la bibliothèque
crossbeam; \n - Files d'attente asynchrones avec
tokio. \n
Chaque approche sera accompagnée d'un exemple de code fonctionnel avec des explications. Vous apprendrez à choisir le bon outil pour une tâche spécifique et à éviter les erreurs courantes.
\n\n1. Canaux classiques : std::sync::mpsc
\n\nLa bibliothèque standard de Rust fournit le module std::sync::mpsc — une file d'attente multithread avec un seul consommateur. C'est le choix idéal lorsque vous avez plusieurs producteurs (threads qui envoient des tâches) et un seul worker (thread qui les exécute).
Exemple simple : pool d'un seul worker
\n\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::Duration;\n\nfn main() {\n // Création du canal : sender (expéditeur) et receiver (récepteur)\n let (tx, rx) = mpsc::channel::<String>();\n\n // Lancement du thread worker\n let worker = thread::spawn(move || {\n for task in rx {\n println!("[Worker] J'exécute la tâche : {}", task);\n thread::sleep(Duration::from_millis(500));\n println!("[Worker] Tâche '{}' terminée !", task);\n }\n });\n\n // Création de plusieurs producteurs (dans un seul thread pour simplifier)\n let producer = thread::spawn(move || {\n let tasks = vec![\n "Traiter le fichier A",\n "Envoyer un email",\n "Mettre à jour la base de données",\n "Générer un rapport",\n ];\n for task in tasks {\n println!("[Producteur] J'envoie : {}", task);\n tx.send(task.to_string()).unwrap();\n thread::sleep(Duration::from_millis(200));\n }\n });\n\n producer.join().unwrap();\n // Une fois que le sender (tx) est détruit, le receiver (rx) reçoit None et la boucle se termine\n drop(tx); // fermeture du canal côté producteur\n worker.join().unwrap();\n}\n\nCaractéristiques :
\n- \n
- Le canal
mpscest bufferisé — les tâches ne bloquent pas l'expéditeur tant que le buffer n'est pas plein. \n - Un seul récepteur — c'est une limitation, mais elle garantit simplicité et sécurité des threads. \n
- Lorsque tous les expéditeurs tombent, le canal se ferme automatiquement. \n
Plusieurs workers : distribution manuelle
\n\nSi vous avez besoin de plusieurs workers, mpsc ne convient pas directement. La solution est de créer un seul canal et plusieurs threads qui vont se concurrencer pour recevoir les tâches. Cependant, cela nécessite de la prudence : Receiver n'implémente pas Sync, il faut donc l'envelopper dans un Mutex ou un Arc.
use std::sync::{mpsc, Arc, Mutex};\nuse std::thread;\nuse std::time::Duration;\n\nfn main() {\n let (tx, rx) = mpsc::channel::<String>();\n let rx = Arc::new(Mutex::new(rx));\n\n let mut workers = vec![];\n for id in 0..3 {\n let rx = Arc::clone(&rx);\n let worker = thread::spawn(move || {\n loop {\n let task = {\n // Acquisition du mutex et réception de la tâche\n let rx = rx.lock().unwrap();\n rx.recv()\n };\n match task {\n Ok(task) => {\n println!("[Worker