Achille 746
ACHILLE746
ExpertisesProcessusRésultatsPortfolioTechnologiesBlogFAQ
Lancer un projet
Accueil›Blog›Rust›Rust async/await et Tokio : comprendre le runtime en profondeur
⚡RUST

Rust async/await et Tokio : comprendre le runtime en profondeur

27 JUIN 2026•Par l'équipe Achille 746•10 min de lecture

L'async/await de Rust est l'une des fonctionnalités les plus puissantes du langage — et l'une des plus mal comprises. Contrairement à l'async de JavaScript, Node.js ou Python, le modèle async de Rust ne repose pas sur un runtime intégré au langage. Il définit un contrat — le trait Future— et délègue l'exécution à un runtime externe. Tokio est ce runtime pour la quasi-totalité des applications en production. Comprendre comment async fn, Future et Tokio interagissent est indispensable pour écrire du code async performant, éviter les pièges classiques, et déboguer efficacement les problèmes de concurrence.

Le modèle fondamental : les Futures sont paresseuses

La différence la plus importante entre Rust et les autres langages async est que les Futures sont paresseuses. En JavaScript, fetch(url)lance immédiatement la requête HTTP, qu'on attende le résultat ou non. En Rust, reqwest::get(url) crée une valeur de typeFuture qui ne fait rien jusqu'à ce qu'elle soit polled. Si vous créez une future et ne l'attendez jamais (ni .await, ni tokio::spawn), elle ne s'exécute pas du tout.

Cette paresse est délibérée. Elle donne à Rust un contrôle total sur le scheduling : la future ne fait rien jusqu'à ce qu'un exécuteur décide de la faire avancer. Chaque fois que l'exécuteur appelle poll() sur la future, elle s'exécute jusqu'au prochain point .await, puis rend le contrôle. Si l'opération n'est pas terminée (typiquement, on attend des données réseau), elle retourne Poll::Pendinget enregistre un waker qui sera appelé quand les données seront disponibles. L'exécuteur peut alors aller faire avancer d'autres futures pendant ce temps.

Ce modèle dit poll-based ou zero-cost asyncsignifie qu'une future en attente n'occupe pas de thread. Des millions de connexions TCP peuvent être maintenues par un pool de quelques dizaines de threads — c'est ce qui permet à Tokio d'atteindre des performances que les modèles thread-per-connection ne peuvent pas approcher.

Le trait Future et le sucre syntaxique async/await

Le trait Future est simple :

pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

Écrire des implémentations de Future à la main est fastidieux et verbeux. Le mot-cléasync fn est du sucre syntaxique qui transforme automatiquement une fonction en un générateur de state machine implémentant Future. Le compilateur Rust génère le code de la machine d'état qui encode les points de suspension (.await) et les variables locales qui doivent être préservées entre deux polls. C'est pour cette raison que les futures Rust ont une taille fixe connue à la compilation — leur empreinte mémoire est exactement celle des variables locales qu'elles doivent préserver.

Pinning : les futures Rust doivent être épinglées en mémoire (Pin<&mut Self>) avant d'être polled. Cela garantit que la future ne sera pas déplacée en mémoire entre deux appels à poll(), ce qui est nécessaire pour les auto-références internes à la state machine. Dans la pratique quotidienne, Box::pin et tokio::spawn gèrent le pinning automatiquement — vous ne manipulez Pin directement que si vous implémentez Futureà la main.

Tokio : architecture du runtime

Tokio est un runtime async multi-thread avec un scheduler work-stealing. Son architecture repose sur trois composants clés. Le thread pool: par défaut, Tokio crée autant de threads qu'il y a de cœurs CPU. Chaque thread exécute une boucle événementielle qui poll les futures prêtes. Leréacteur I/O: une thread dédiée surveille les événements kernel (epoll sur Linux, kqueue sur macOS, IOCP sur Windows) et réveille les futures dont l'I/O est prête via leur waker. Lescheduler: quand une future est prête, son task est ajoutée à la queue du thread qui la possède. Si ce thread est occupé, d'autres threads peuvent la “voler” (work-stealing) pour maximiser l'utilisation des cœurs.

Tasks, spawn et spawn_blocking

tokio::spawn crée une task— une future qui s'exécute de façon indépendante sur le runtime Tokio. C'est l'équivalent léger d'un thread vert. Des centaines de milliers de tasks peuvent coexister sans problème de mémoire, car chaque task n'occupe que la mémoire de ses variables locales (typiquement quelques centaines d'octets).

La règle d'or à ne jamais violer : ne jamais bloquer dans une tâche async. Un appel bloquant (std::thread::sleep, une lecture de fichier synchrone, un calcul CPU long) dans une task Tokio bloque le thread entier et empêche toutes les autres tasks qui lui sont assignées de s'exécuter. C'est un deadlock de runtime — invisible au compilateur, catastrophique en production. Pour les opérations bloquantes, tokio::task::spawn_blocking exécute la closure sur un pool de threads dédiés (distinct du pool async), sans bloquer le runtime. Pour les APIs Axum en production, ce pattern est utilisé pour les requêtes de base de données synchrones ou les opérations de compression CPU.

Concurrence avec join! et select!

tokio::join! exécute plusieurs futures concurremmentsur la même task et attend que toutes se terminent. C'est l'outil pour paralléliser des opérations indépendantes sans créer de tasks séparées :

let (user, posts) = tokio::join!(
    fetch_user(user_id),
    fetch_posts(user_id)
);

tokio::select! exécute plusieurs futures et retourne dès que la premièrese termine, en annulant les autres. C'est l'outil pour les timeouts, les races entre plusieurs sources, et les cancellations :

tokio::select! {
    result = fetch_data() => handle(result),
    _ = tokio::time::sleep(Duration::from_secs(5)) => {
        return Err(Error::Timeout);
    }
}

Attention à la cancellation : quand select! annule une future, elle est droppée immédiatement. Une future qui tient un verrou (Mutex guard) ou une transaction de base de données doit s'assurer que ces ressources sont correctement libérées dans leur destructeur. Les futures qui ne supportent pas la cancellation propre doivent être encapsulées danstokio::spawn + JoinHandle::abort()pour contrôler finement le moment de l'annulation.

Pièges courants et comment les éviter

  • Mutex std dans du code async : std::sync::Mutex est bloquant par nature. Le tenir à travers un .await bloque le thread Tokio. Utiliser tokio::sync::Mutex(qui suspend la task plutôt que bloquer le thread) ou restructurer pour ne pas tenir le lock pendant les points d'attente.
  • Futures non-Send dans spawn : tokio::spawn exige que les futures implémententSend (elles peuvent être envoyées entre threads). Les Rc, les raw pointers, et les types contenant des références non-Send ne peuvent pas être utilisés dans des tasks multi-thread. Tokio propose spawn_local pour les futures non-Send, à utiliser avec LocalSet.
  • Trop de tasks pour du CPU-bound: l'async est optimisé pour l'I/O-bound. Pour du calcul CPU intensif (compression, cryptographie, traitement d'image), spawn_blockingou rayon sont plus adaptés que des centaines de tasks qui se battent pour les threads CPU.

“L'async Rust est le contrat le plus honnête qui soit : zéro magie, zéro runtime caché, zéro allocation surprise. Mais il exige que vous compreniez exactement ce qui se passe sous le capot.”

Maîtriser l'async Rust demande un investissement initial mais ouvre la voie à des architectures concurrentes d'une efficacité exceptionnelle. Les serveurs Axum et les daemons système écrits en Rust avec Tokio atteignent régulièrement des millions de requêtes par seconde sur du matériel modeste, avec une prévisibilité de latence impossible à obtenir avec des runtimes garbage-collectés. Pour concevoir des systèmes async Rust en productionadaptés à vos contraintes de performance et de fiabilité, nous intervenons de l'architecture à l'optimisation fine.

Article précédentGestion des secrets en production : Vault, SOPS et rotation automatiqueTous les articlesArticle suivantCréer des outils CLI professionnels en Rust : clap, indicatif, distribution
Partager cet article :Twitter / XLinkedIn

Articles liés

🦀
Rust en 2026 : le langage le plus sécurisé pour les applications critiques
8 min de lecture
→
⚡
Axum et Tokio : construire une API REST haute performance en Rust
10 min de lecture
→
🦀
La gestion d'erreurs en Rust : Result, ? operator et patterns de production
8 min de lecture
→
🦀
Créer des outils CLI professionnels en Rust : clap, indicatif, distribution
9 min de lecture
→

Vous construisez des systèmes concurrents haute performance en Rust ?

Nous concevons et optimisons des architectures async Rust — serveurs, daemons, workers — pour des systèmes où la latence et la fiabilité sont critiques.

Discuter de votre projet →