Async Rust n’a jamais dépassé le stade MVP
(tweedegolf.nl)- Async Rust permet d’exécuter le même code, indépendant de l’exécuteur, sur des serveurs et des microcontrôleurs, mais la machine à états générée par le compilateur entraîne une augmentation notable de la taille des binaires, en particulier dans l’embarqué
- Même un exemple simple comme
bar(), avec deux points d’attente, génère 360 lignes de MIR ainsi que les étatsUnresumed,Returned,Panicked,Suspend0etSuspend1, alors que la version synchrone n’a besoin que de 23 lignes - Remplacer le
panicaprès un nouveaupolld’un future déjà terminé par un retour dePoll::Pendingpermet de respecter le contrat sans comportement unsafe, et les expériences montrent une réduction de 2 % à 5 % de la taille des binaires sur des firmwares embarqués - Même
async { 5 }, sansawait, construit aujourd’hui une machine à états avec 3 états de base, mais l’optimiser pour renvoyer systématiquementPoll::Ready(5)réduit de 0,2 % la taille des binaires embarqués - Le Project Goal proposé vise à faire progresser côté compilateur la suppression du panic après complétion en mode release, la suppression de la machine à états pour les blocs async sans
await, l’inlining des futures àawaitunique, et la fusion d’états identiques
Le problème de gonflement au niveau compilateur dans Async Rust
- Async Rust permet d’exécuter un même code indépendant de l’exécuteur sur des serveurs et des microcontrôleurs, mais sur les petits microcontrôleurs l’augmentation de la taille des binaires est particulièrement visible
- Le blog Rust a présenté async/await comme une abstraction à coût nul, mais en pratique async provoque beaucoup de gonflement, y compris sur desktop et serveur ; le problème y est simplement moins visible grâce à des ressources mémoire et CPU plus abondantes
- Après des contournements pour éviter ce gonflement lors de l’écriture de code async, un Project Goal a été soumis pour traiter le problème dans le compilateur
- Le problème des futures inutilement volumineux et des copies excessives est exclu du périmètre
- Le sujet est déjà connu, et une PR qui en traite une partie est ouverte : https://github.com/rust-lang/rust/pull/135527
Structure des futures générés
- Dans l’exemple,
foo()retourneasync { 5 }etbar()exécutefoo().await + foo().await- Exemple Godbolt : godbolt
barcomporte deux points d’attente, donc la machine à états a besoin d’au moins deux états, mais en pratique le compilateur en génère davantage- Le compilateur Rust peut dumper le MIR à plusieurs passes, et la passe
coroutine_resumeest la dernière passe MIR spécifique à async- async existe encore dans le MIR, mais plus dans le LLVM IR ; la transformation d’async en machine à états se produit donc dans les passes MIR
- La fonction
bargénère 360 lignes de MIR, tandis que la version synchrone n’en utilise que 23 - Le
CoroutineLayoutproduit par le compilateur correspond en pratique à un ensemble d’états sous forme d’enumUnresumed: état initialReturned: état terminéPanicked: état après panicSuspend0: premier point d’attente, qui stocke le future defooSuspend1: second point d’attente, qui stocke le premier résultat et le second future defoo
Future::pollest une fonction sûre ; même si on l’appelle de nouveau après la fin d’un future, cela ne doit pas provoquer d’UB- Aujourd’hui, après
Suspend1, il renvoieReadypuis fait passer le future à l’étatReturned - Un nouveau
polldans cet état provoque un panic
- Aujourd’hui, après
- L’état
Panickedsemble servir à empêcher qu’un future soit repollé après qu’une fonction async a paniqué puis que ce panic a été intercepté aveccatch_unwind- Après un panic, le future peut être dans un état incomplet ; le repoller pourrait donc mener à de l’UB
- Ce mécanisme ressemble beaucoup au mutex poisoning
- Cette interprétation de l’état
Panickedn’est pas documentée de manière totalement claire ; le niveau de confiance annoncé est d’environ 90 %
Faut-il vraiment paniquer lors d’un poll après complétion ?
- Les futures à l’état
Returnedpaniquent actuellement, mais ce n’est pas une obligation- La seule contrainte nécessaire est d’éviter toute UB
- Un panic est relativement coûteux et ajoute un chemin à effets de bord difficile à éliminer par optimisation
- Si un future déjà terminé retourne
Poll::Pendinglorsqu’il est repollé, le contrat du typeFuturepeut être respecté sans comportement unsafe - Après modification du compilateur pour tester cette approche, une réduction de 2 % à 5 % de la taille des binaires a été observée sur des firmwares embarqués async
- Il est proposé d’exposer ce comportement via un switch, à la manière de
overflow-checks = falsepour les dépassements d’entiers- En build debug, le panic resterait en place pour révéler immédiatement un comportement incorrect
- En build release, on obtiendrait des futures plus petits
- Avec
panic=abort, il serait peut-être possible de supprimer complètement l’étatPanicked, mais l’impact doit être étudié plus en détail
Une machine à états est générée même sans await
foo()ne retourne queasync { 5 }, donc la forme optimale en implémentation manuelle serait un future sans état qui retourne toujoursPoll::Ready(5)- Pourtant, le MIR généré par le compilateur contient toujours les trois états de base
Unresumed,ReturnedetPanicked- Lors du
poll, le discriminant de l’état courant est vérifié puis un branchement est effectué - Si le future est repollé après sa complétion, cela déclenche un panic avec
`async fn` resumed after completion
- Lors du
- Dans ce cas, on peut optimiser en supprimant totalement la machine à états et en retournant simplement
Poll::Ready(5)à chaque appel - Une application expérimentale de cette idée dans le compilateur a réduit de 0,2 % la taille des binaires embarqués
- Le gain est modeste, mais l’optimisation est simple, donc elle peut valoir le coup
- Cette optimisation modifie légèrement le comportement, mais seuls les exécuteurs qui ne respectent pas le contrat seraient affectés
- Aujourd’hui, le compilateur déclenche un panic lors d’un
pollultérieur - Après optimisation, le future retournerait toujours
Ready
- Aujourd’hui, le compilateur déclenche un panic lors d’un
LLVM seul ne suffit pas
- Même si le MIR produit est inefficace, LLVM peut parfois tout nettoyer, mais seulement sous certaines conditions
- Le future doit être suffisamment simple
- Il faut utiliser
opt-level=3
- Dès que le future devient plus complexe, LLVM n’élimine plus tout ; or dans le code Async Rust idiomatique, les futures sont profondément imbriqués, ce qui fait rapidement exploser la complexité
- Dans les environnements où l’on optimise fréquemment pour la taille, comme l’embarqué ou wasm, LLVM n’arrive pas à tout optimiser
- Exemple Godbolt : https://godbolt.org/z/58ahb3nne
- Dans l’assembleur généré, LLVM comprend bien que
fooretourne 5, mais n’optimise pas le résultat debaren 10 - L’appel à la fonction
polldefooest toujours présent - Cela vient de chemins de panic potentiels que le compilateur ne parvient pas à écarter complètement
- LLVM ne sait pas que
foon’est réellement appelé qu’une seule fois et qu’il ne panique pas
- Dans l’assembleur généré, LLVM comprend bien que
- Si l’on commente la branche de panic dans l’IR, l’optimisation devient meilleure : https://godbolt.org/z/38KqjsY8E
- Plutôt que d’attendre une optimisation a posteriori de LLVM, le compilateur doit fournir à LLVM une meilleure entrée
L’inlining des futures fonctionne mal
- L’inlining est important parce qu’il ouvre la voie aux passes d’optimisation suivantes, mais les futures générés par Rust ne sont actuellement pas inlinés à un stade suffisamment précoce
- Chaque future obtient bien une implémentation, puis LLVM et l’éditeur de liens ont une occasion de l’inliner, mais à cause des problèmes précédents cela arrive trop tard
- Le cas d’inlining le plus direct est celui où
bar()ne fait quefoo(blah).await- C’est un motif fréquent quand on construit des abstractions avec des traits
- Aujourd’hui, le compilateur génère une machine à états pour
bar, puis appelle à l’intérieur la machine à états defoo - Une approche plus efficace serait que
barsoit directement le future defoo
- Lorsqu’il existe un préambule et un postambule, le cas devient plus complexe
- Exemple :
bar(input)construitblahavecinput > 10, puis exécutefoo(blah).awaitavant d’appliquer* 2au résultat - C’est fréquent lors de la transformation d’une fonction async vers une autre signature, notamment dans des implémentations de trait
- Exemple :
- Même sous cette forme,
barn’a pas besoin de son propre état async- Aucune donnée, en dehors de la valeur capturée par
foo, n’a besoin d’être conservée au-delà de l’unique point d’attente - En revanche,
barne peut pas être simplement remplacé parfoolui-même ; il peut surtout s’appuyer sur l’essentiel de l’état defoo
- Aucune donnée, en dehors de la valeur capturée par
- Dans une implémentation manuelle,
BarFutpourrait avoir les étatsUnresumed { input }etInlined { foo: FooFut }- Au premier
poll, il exécute le préambule, créefoo(blah)puis passe à l’étatInlined - Ensuite, il applique le postambule au résultat de
foo.poll(cx)
- Au premier
- On pourrait aussi supprimer l’état
Unresumedsi l’on exécutait le code avant le premier point d’attente à l’avance, mais le contrat garantit qu’un future ne fait rien tant qu’il n’a pas étépoll - Si l’on pouvait interroger les propriétés d’un future en cours de
poll, d’autres optimisations d’inlining deviendraient possibles- Par exemple, si l’on sait qu’un future retourne toujours immédiatement ready au premier
poll, le future appelant n’a pas besoin de créer un état pour ce point d’attente - En appliquant récursivement cette optimisation, beaucoup de futures pourraient être réduits à des machines à états bien plus simples
- Par exemple, si l’on sait qu’un future retourne toujours immédiatement ready au premier
- Dans l’architecture actuelle de
rustc, chaque bloc async semble être transformé séparément et les données utiles ne sont pas conservées ensuite, ce qui empêche ce type d’interrogation - L’inlining des futures n’a pas encore été expérimenté, mais il devrait apporter des gains importants en taille de binaire et en performances
Fusion d’états identiques
- Chaque point d’attente d’un bloc async ajoute un état supplémentaire à la machine à états
- Un code comme le suivant est naturel, mais comme les deux branches attendent la même fonction async, il crée deux états identiques
CommandId::A => send_response(123).awaitCommandId::B => send_response(456).await
- Dans ce cas, le
CoroutineLayoutcrée_s0et_s1, qui stockent chacun le même type de coroutine desend_response, ainsi que deux étatsSuspend0etSuspend1 - Le MIR de cette fonction comporte 456 lignes, et nombre de blocs de base sont en pratique des doublons
- En refactorisant manuellement le code pour calculer d’abord la valeur de réponse, puis faire un seul
send_response(response).await, ces états redondants disparaissentCommandId::Adonne123CommandId::Bdonne456- puis
send_response(response).await
- Après refactorisation, le
CoroutineLayoutne contient plus qu’un seul future stocké et un unique étatSuspend0 - La longueur totale du MIR tombe à 302 lignes, et la duplication disparaît
- Une passe d’optimisation capable de détecter et de fusionner les chemins de code et états identiques semble donc utile
- Cette optimisation pourrait très bien se combiner avec une passe d’inlining des futures
Liens d’expérimentation et benchmarks supplémentaires
- En appliquant ensemble les deux expériences, on obtient environ 3 % de performances en plus sur un benchmark synthétique x86 utilisant l’exécuteur
smol - No panics in poll after ready: https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending
- No await, no statemachine: https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await
Appel à soutien pour le Project Goal
- Ce travail a été soumis comme Project Goal afin d’avancer côté compilateur : https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
- Sans financement, il sera difficile de faire beaucoup progresser le chantier ; un soutien partiel ou total d’entreprises ou d’organisations qui en bénéficieraient est donc nécessaire
- Le contact indiqué est
dion@tweedegolf.com - Le périmètre du travail et le montant nécessaire sont flexibles, mais 30 k€ seraient, d’après l’estimation, suffisants pour réaliser tout ou une grande partie du projet
2 commentaires
Commentaires Hacker News
Je suis d’accord pour dire que le titre est un peu exagéré, mais l’article est bien écrit et l’idée principale passe bien
Je n’ai pas encore assez d’expérience avec l’async en Rust pour avoir un avis tranché, mais quelques points m’ont marqué
Le bon côté, c’est qu’on peut avoir un runtime explicite. Au lieu de contaminer tout le projet avec l’async, on peut garder une base synchrone et n’utiliser le runtime qu’aux « frontières » des entrées/sorties
Sur le projet sur lequel je travaille, cette approche a bien fonctionné, et elle ressemble assez à la stratégie adoptée par Zig pour le code d’E/S. Dans ce cas, le problème de la couleur des fonctions était en grande partie résolu, et comme il fallait séparer strictement les E/S du code centré CPU, un runtime d’E/S explicite paraissait naturel
Le mauvais côté, c’est que tout l’écosystème semble trop dépendre de tokio. C’est un peu comme si le GC de Java était optionnel, mais qu’en pratique tout le monde utilisait le même runtime GC tiers, et que n’importe quelle bibliothèque l’impose. Ce type de dépendance centrale n’est pas sain
Les exigences d’un runtime async sur un processeur de station de travail et dans un environnement comme le RP2040 sont très différentes. Malgré cela, comme on peut changer de backend, même en écrivant du code d’E/S async pour un petit microcontrôleur ARM M0, avec embassy — un runtime centré embarqué — le code ressemble presque à celui utilisé ailleurs
Comme on utilise les mêmes traits et interfaces, on peut moins se soucier des détails du runtime. Comparé à l’utilisation d’un petit RTOS ou à la création de son propre environnement async, c’est plutôt bien
Et ce qu’on apprend en écrivant du code async avec embassy se transfère aussi à d’autres domaines
Même si tokio ne fait pas partie de la bibliothèque standard, il est bien maintenu, donc la situation actuelle me paraît correcte. Au contraire, si cela entrait dans la bibliothèque standard, j’aurais peur qu’il devienne plus difficile d’utiliser d’autres exécuteurs, et plus compliqué aussi de porter la bibliothèque standard sur d’autres plateformes
Bien sûr, il est possible que cette inquiétude soit infondée
Le logging est aujourd’hui plus ou moins unifié autour de slf4j, mais il reste des bibliothèques qui utilisent autre chose, et pour les utilitaires communs, c’était d’abord Apache Commons puis aujourd’hui souvent Guava
Pour le JSON, Jackson s’est imposé dans une certaine mesure, mais Gson et Simple-json restent courants, et pour les annotations de nullabilité, on est passé de distributions non officielles du JSR-305 jamais officialisé au checker framework, puis récemment à JSpecify
Ce genre de briques de base devrait être fourni par le langage afin d’éviter la fragmentation et la prolifération de bibliothèques standard de fait
Écrire des bibliothèques indépendantes de l’exécuteur n’est pas très difficile, mais cela demande une vigilance constante, et ce n’est pas quelque chose que l’ensemble de la communauté respecte toujours
Excellent article. J’aime ce type d’analyse d’optimisation en profondeur, et j’espère aussi que les objectifs du projet aboutiront bien
J’ai souvent l’impression que le compilateur ne consacre pas beaucoup d’efforts à l’optimisation des cas « triviaux »
Cela dit, le titre est trop dramatique par rapport au contenu. J’aurais aussi cliqué sur « Async Rust Optimizations the Compiler Still Misses »
On peut maintenant utiliser async dans les traits et les closures, mais c’est une mise à jour du système de types, pas un changement de la mécanique async elle-même. Les Waker sont aussi devenus un peu plus faciles à manipuler, mais cela relève davantage d’améliorations côté std/core
Si j’ai bien compris, les personnes qui ont fait atterrir l’async en Rust ont pas mal souffert de burnout et sont devenues moins actives, et presque personne n’a vraiment pris le relais. Cela dit, je suis assez content que des gens de Google aient ouvert une PR pour optimiser la disposition mémoire des variables capturées
Mes collègues et moi utilisons beaucoup l’async, donc il se peut qu’on doive le faire nous-mêmes, ou au moins s’y mettre. On est peut-être plus proche du sens où « gratuit » signifie « comme un chiot gratuit »
Donc oui, le titre est un peu putaclic, mais je n’ai pas l’intention de le retirer
L’auteur semble obsédé par l’overhead de fonctions triviales. Il est gêné par l’overhead des états « panic » et « returned », mais ce n’est pas un gros problème
La plupart des blocs async utiles sont suffisamment gros pour que l’overhead des cas d’erreur soit noyé dedans
Il y a peut-être quelque chose à dire sur le manque d’inlining. Mais ce qui limite le nombre massif d’activités, c’est en général l’espace d’état requis par chacune
L’async me semble globalement être une idée encore immature. Le code ordinaire était déjà asynchrone
Si on doit attendre une tâche async, le thread dort jusqu’à ce qu’elle soit prête, et le noyau abstrait cela. Mais comme on n’aimait pas structurer le code avec des threads logiques, on a ajouté un système de callbacks pour les événements, puis on s’est rendu compte que les callbacks sont difficiles à raisonner et que le contrôle séquentiel est préférable
Donc je pense que les threads étaient le bon modèle de programmation
Maintenant, les runtimes de langage préfèrent les « green threads » pour des raisons de portabilité et de performance, mais la plupart des langages ne les fournissent pas correctement. À la place, on se retrouve avec les problèmes de couleur async/non-async, d’ordonnancement, de priorité, de non-préemption. C’est un modèle d’ordonnancement et de processus pire que dans les années 1970
Même le code async est souvent écrit de manière à ne pas maximiser le parallélisme exprimable. Par exemple, on écrit « await process(x) pour chaque tâche X » au lieu de « lancer simultanément les N tâches d’E/S »
Mais dans le monde des threads, ce problème de parallélisme est encore pire. Les threads sont intrinsèquement trop lourds pour exprimer efficacement la concurrence, et il n’y a aucun moyen de les optimiser dans cette direction
Ce n’est pas une leçon nouvelle. On sait depuis longtemps que les exécuteurs à work-stealing ont une latence bien plus faible et des P99 plus stables que les threads traditionnels. C’est d’ailleurs pour cela qu’Apple a créé GCD au début des années 2000
Les threads ne fournissent pas au scheduler du noyau les informations plus riches dont il aurait besoin pour comprendre la charge de travail, et les threads noyau sont un mécanisme bien trop lourd pour obtenir une concurrence fine. C’est encore pire quand il ne s’agit pas de calcul pur mais d’E/S ou de charges mixtes
Tous les programmes n’ont pas besoin de ce niveau de performance, mais à effort égal, il est beaucoup plus simple d’atteindre un niveau de performance supérieur, et en pratique on obtient des latences et des débits que l’approche traditionnelle a du mal à suivre
Un autre signal que l’async va dans la bonne direction se voit avec io_uring. L’approche haute performance des E/S dans le noyau avec io_uring est totalement différente du threading traditionnel et des appels système, et le traitement des complétions ressemble bien davantage à la concurrence async. Cela dit, async/await à lui seul n’a pas assez de « couleurs » pour exprimer les relations entre tâches async, donc l’exploiter pleinement reste plus difficile
La dernière fois que j’ai travaillé sur du code de coroutines / scheduling, créer un thread qui se termine immédiatement puis faire un join prenait environ 200µs, alors que créer, planifier puis attendre un green thread maison prenait environ 400ns
Pas besoin d’attendre 10 ans qu’un autre conçoive un framework async absurdement complexe. Dans n’importe quel langage système, 20 lignes d’assembleur suffisent pour faire ses propres green threads / coroutines avec pile
L’optimisation du code centré bande passante est une question de conception de l’ordonnancement. Dans le modèle multithread classique, on ne contrôle le scheduling que de manière limitée, alors que dans un modèle async on peut le contrôler presque parfaitement
Un ordonnanceur async bien optimisé est énormément plus rapide qu’une architecture multithread équivalente sur ce type de charge centrée bande passante, à un point qui n’est même pas comparable
Aujourd’hui, la plupart du code haute performance est centré bande passante, et l’async existe pour optimiser plus facilement ce type de charge
Quand on teste le traitement concurrent et qu’on vérifie si les race conditions sont correctement gérées, les callbacks rendent cela bien plus simple parce qu’ils permettent de contrôler l’ordonnancement. Chaque callback représente une unité distincte, donc on voit quels événements peuvent être réordonnés, et on peut plus facilement examiner différents ordres
Avec les threads, au contraire, il est facile d’ignorer l’ordre, et de ne pas réfléchir à quel moment la complexité produite par les autres threads peut affecter le thread courant. Ce n’est pas vraiment plus simple, c’est plutôt une simplification trompeuse
Et sauf à ajouter des barrières artificielles pour bloquer les threads, ou à remplacer les E/S par des stubs et injecter des mocks avec callbacks pour contrôler l’ordre, il est difficile de réellement faire varier les scénarios concurrents dans les tests
Le problème des callbacks, c’est que la pile d’appels capturée n’est pas la pile d’appels logique. À moins d’utiliser certaines bibliothèques/runtimes qui ont travaillé à rendre cela utile, il faut une bonne définition des erreurs
Bien sûr, on peut aussi mélanger les deux paradigmes et n’en récupérer que les défauts
Si l’objectif principal de Rust est la sûreté, je ne comprends pas pourquoi il y a panic. On devrait pouvoir prouver qu’il n’existe absolument aucun chemin pouvant paniquer dans le code
J’ai regardé cela toute la semaine, et il est très difficile de produire un programme dont on peut garantir qu’il ne paniquera jamais. D’après ce que je comprends, le panic handler pèse environ 300 Ko, et le seul moyen de l’exclure est qu’il n’existe aucun chemin de panic possible dans le code à la compilation. Vérifier après compilation si le binaire contient le panic handler ressemble à un hack
On peut interdire
unwrapet les autres opérations qui paniquent via des lints, mais s’il existait un sous-ensemble no-panic de Rust, une bonne partie des problèmes abordés dans cet article disparaîtraitIl est frustrant d’avoir affaire à un langage où trop d’opérations peuvent théoriquement paniquer, alors qu’en pratique cela n’arriverait qu’au niveau d’un bit flip ou équivalent. C’est pareil quand il faut prouver qu’un tableau n’est pas vide ou quand on manipule l’async
Au final, on se retrouve soit à ajouter énormément de gestion d’erreurs pour des situations qui ne se produiront jamais, soit à utiliser des structures étranges comme le motif de liste non vide avec un premier champ et le reste séparé. Et cette structure ajoute elle-même son propre gonflement
Des travaux progressent aussi lentement pour augmenter les usages fondés sur des preuves, y compris la capacité à prouver qu’un tableau n’est pas vide
S’il n’y avait pas de panic et qu’il fallait continuer l’exécution dans tous les cas, il faudrait ajouter énormément de gestion d’erreurs partout où l’on vérifie des invariants pour tenter de récupérer de situations comme une corruption mémoire ayant cassé ces invariants
Ce serait exactement le même type de problème que celui qui vous inquiète : une énorme masse de gestion d’erreurs pour des situations qui n’arriveront pratiquement jamais
Il y a quelque chose de fatigant dans cette attitude qui consiste à vouloir qu’un outil rende tout infalsifiable sans rien faire soi-même. On veut une API facile, puis si ce n’est pas assez facile on veut des conteneurs Kubernetes « programmés » en YAML, et si ce n’est toujours pas assez facile on veut les services d’hébergement à clics de GCP ou Amazon
Au fond, ce n’est plus vraiment vouloir programmer, c’est vouloir consommer des applications qui ne tombent jamais en panne, et ce mode de vie repose seulement sur une relation symbiotique avec les gens qui fabriquent réellement les choses
Ce type de discussion laide mais nécessaire existe aussi depuis longtemps en C++
Dès l’introduction d’async en Rust, son caractère contagieux ne me plaisait pas
J’espère que Rust réussira, et si davantage de personnes comme celles-ci s’y mettent, l’avenir de Rust pourrait être plus prometteur
J’ai commencé récemment un travail sur l’async en Rust, et le principal problème que je rencontre pour l’instant, c’est la duplication de code
Chaque fonction pour laquelle je veux supporter à la fois une API asynchrone et une API bloquante doit être écrite en double. Quelque chose comme
maybe-asyncserait appréciableJ’ai regardé des crates comme maybe-async ou bisync pour contourner cela, mais elles avaient toutes des problèmes ou de fortes contraintes
asyncouconstAujourd’hui, le meilleur choix pour écrire du code que l’on veut faire vivre à la fois dans le monde synchrone et asynchrone, c’est le sans-io. Thomas Eizinger de Fireguard a écrit un bon article sur ce pattern[1]
Ce pattern ne résout pas proprement seulement le problème sync/async, il facilite aussi les tests et ouvre la voie à des techniques comme DST[2]
J’ai aussi écrit un article sur le sujet[3], où j’insiste sur le fait que le problème dépasse la simple opposition async vs sync et englobe plus largement les différents exécuteurs
0: https://github.com/rust-lang/effects-initiative
1: https://www.firezone.dev/blog/sans-io
2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
3: https://hugotunius.se/2024/03/08/on-async-rust.html
asyncest déjà une forme demaybe-asyncLa différence entre
fn -> voidetfn -> Future, c’est que la première s’exécute immédiatement jusqu’au bout, alors que la seconde peut ne se terminer que plus tardSi vous voulez exécuter une fonction async de manière bloquante, il suffit d’utiliser un exécuteur bloquant
J’aime cet article parce qu’il m’a aussi fait découvrir les objectifs Rust 2026
Mon équipe utilise Rust, mais nous n’avons pas eu besoin d’entrer très profondément dans le langage pour faire ce dont nous avions besoin. Cela dit, c’est agréable de voir un langage se développer à partir de sa base avec autant de retours de la communauté
Je n’ai jamais vraiment eu cette impression avec le C++, et je ne sais pas très bien comment cela se passe dans d’autres domaines
Le seul point un peu décevant, c’est que chaque objectif semble demander un financement spécifique, ce qui donne un petit côté Kickstarter. Je me demande si c’est vraiment le meilleur modèle trouvé jusqu’ici
Un objectif de projet est un système qui permet à une personne ou à un petit groupe d’exprimer qu’ils veulent travailler sur quelque chose, et de demander aux bénévoles du projet Rust un soutien durable en temps, comme des revues de code ou des réponses à des questions
Cela ne veut pas dire que le projet Rust lui-même s’est fixé cet objectif, ni qu’il le soutient nécessairement
Il n’est donc pas correct de le voir comme une feuille de route officielle de Rust ; il vaut mieux y voir quelque chose comme « il y a des contributeurs qui veulent travailler dans ce domaine »
Malheureusement, une fois qu’une technologie s’installe commercialement, cela semble souvent évoluer de cette façon. Il est difficile d’en vouloir aux gros sponsors de ne financer que les aspects qui les intéressent
Heureusement, une part importante du financement de TweedeGolf viendrait du gouvernement néerlandais
Les nouvelles fonctionnalités, cela se « vend ». Leur développement coûte de l’argent, mais elles résolvent de vrais problèmes, et si le coût de ces problèmes dépasse celui du développement, les entreprises sont généralement prêtes à payer
La maintenance est plus difficile, mais il existe désormais aussi des fonds pour les mainteneurs. Le fonds de RustNL en est un exemple : https://rustnl.org/maintainers/
Ces fonds visent un travail plus large et plus durable, soutenu par les petites contributions de plusieurs organisations
Je ne sais pas si c’est le meilleur modèle, mais au moins cela semble fonctionner dans une certaine mesure
En lisant la documentation de Rust Async et de Tokio, on voit bien expliqué pourquoi il ne faut pas mettre les parties CPU-intensives dans la pile async, comment utiliser efficacement des outils de base comme
std::sync::Mutexdans des blocs async, et comment raccorder du code synchrone et du code asyncBeaucoup de code ne suit pas ces recommandations parce que l’efficacité n’est ni une priorité ni une nécessité. Mais il existe de nombreux projets où la performance et l’efficacité comptent, et une fois le code en production, on finit par voir les pièges. ScyllaDB en est un exemple
Les LLM n’aident pas non plus. Ils rendent tout async jusqu’à
main, utilisent les mauvais outils de base et ne conçoivent pas correctement le systèmeLe repli d’états dupliqués, c’est-à-dire le pattern qui consiste à remonter le
matchhors des branchesawaitcomme dans l’exempleprocess_command, est probablement la chose la plus simple que n’importe qui puisse appliquer dès aujourd’hui sur du code async existantCela ne demande aucun travail du compilateur, seulement du refactoring
À propos de « les Future ne s’inlinent pas facilement », dans le langage de programmation que j’ai créé, j’ai écrit un pass personnalisé qui inline les appels de fonctions async dans des fonctions async
Globalement cela marche bien et permet d’éliminer un peu de boilerplate, mais la taille du binaire résultant augmente beaucoup
Techniquement, Rust pourrait faire la même chose
Avis sur Lobste.rs
C’était un article bien plus constructif que ce à quoi je m’attendais en voyant seulement le titre
J’espère que celles et ceux qui veulent travailler là-dessus recevront le soutien nécessaire
Je suis content de voir que ce problème est traité. J’ai déjà vu plusieurs fois des billets expliquant qu’en ce moment rustc transmet beaucoup trop de code à LLVM en espérant que l’optimiseur règle tout, et cet article demande en plus un financement pour ce travail
Mon Dieu, j’étais idiot
J’ai toujours pensé que l’async était intrinsèquement « volumineux », puisqu’il faut forcément, sous une forme ou une autre, un runtime, un suivi des tâches et du polling pour vérifier l’achèvement. Donc cette surcharge n’est pas nulle
Je pensais que la « zéro-cost abstraction » dont on parlait concernait la fonctionnalité du langage, indépendamment du runtime ajouté
Il ne m’était même jamais venu à l’esprit de regarder ce que rustc émet avant de le passer à LLVM
Pour les personnes peu familières avec async Rust :
C’est tout à fait vrai. Même un arbre imbriqué d’appels async se fige, après optimisation maximale, en une seule struct avec une machine à états en interne. C’est vraiment ingénieux
Si on atteint ce cas en build de release, est-ce qu’on obtient une sorte de deadlock ? Ou bien y a-t-il aussi un risque de fuite à cause de tâches qui attendent un travail qui reste toujours sur
Pending?On ne peut pas faire un polling incorrect avec
.awaitQuelques réflexions :
panic=unwind. En dehors de certains test harnesses, je vois rarement des avantages qui compensent son coût par rapport àpanic=abort. Même pour les test harnesses, il me semble qu’on pourrait faire un choix similaire sous Linux, de manière un peu obscure, avecclonepour faire unwaitsur le thread d’exécution au lieu d’utiliserpthread_join. Je peux me tromper là-dessusLe lien a aussi cessé de marcher chez d’autres personnes ?
Édition : le billet de blog s’affiche une demi-seconde puis bascule sur une page 404
Édition 2 : je suis passé par la liste des billets du blog et j’ai cliqué un peu partout, et même en ouvrant cet article depuis la liste, j’arrive sur une page 404. Comment peut-on casser à ce point un blog statique, ou du moins un blog qui devrait l’être ?
Pour ce que ça vaut, j’ai suivi ce qui semble être les mêmes étapes de reproduction et je n’ai jamais obtenu de 404. J’ai essayé sur téléphone et sur desktop, avec JavaScript activé et désactivé. Il est donc possible que ce que tu as rencontré soit plus complexe que ça en avait l’air