Comprendre `Pin` en Rust
(without.boats)- En Rust,
Pinest un élément fondamental introduit pour manipuler en toute sécurité l’état auto-référentiel à l’intérieur des Future produites par async/await - Une Future async enregistre son état à chaque point d’
await, si bien que des champs d’un même objet peuvent référencer d’autres champs du même objet, formant un type auto-référentiel - Les conceptions reposant sur un move constructor, des offset pointers ou
?Moven’ont pas été retenues en raison de problèmes de coût de suivi à l’exécution, de faisabilité à la compilation et de rétrocompatibilité avec les API existantes - La conception finale est
Pin, qui encapsule un pointeur pour placer sa cible dans un pinned typestate ; grâce à l’auto traitUnpin, la plupart des types peuvent toujours être déplacés comme avant - La difficulté de
Pinvient moins de la notion d’immuabilité elle-même que des limites d’un type de bibliothèque ; le reborrowing,Pin::set, la pinned projection et les interactions avecDropnuisent fortement à son ergonomie
Le problème auquel Pin répond
- Dans l’écosystème async de Rust,
Pinet le pinning constituent une base essentielle, mais restent un sujet difficile et souvent mal compris pour celles et ceux qui apprennent l’async en Rust - L’objectif de
Pinn’est pas de permettre aux utilisateurs de créer eux-mêmes des types auto-référentiels uniquement en Rust sûr- Il sert à manipuler en toute sécurité les Future auto-référentielles générées par le compilateur pour les fonctions async, ou les types auto-référentiels construits en code unsafe par des runtimes comme tokio
- Dans l’exemple
async fn bar, au pointfoo(&mut z).await, il faut stocker ensemble dans le même état de Futurezet la FutureFooqui référencez- Un champ de l’objet Future référence alors un autre champ du même objet
- Ce type de Future devient donc un type auto-référentiel
- Si l’objet est déplacé après être entré dans cet état, ses références internes pointent toujours vers l’ancien emplacement mémoire, qui peut correspondre à de la mémoire morte ou avoir été réutilisé pour une autre valeur
- Avant
Pin, Rust permettait de déplacer un objet dès lors qu’on en avait la propriété ou une référence mutable ; il fallait donc un moyen d’exprimer qu’un objet ne devait plus être déplacé à partir d’un certain moment
Les approches qui n’ont pas abouti
-
move constructor
- Un move constructor exécute du code lors du déplacement d’une valeur, à la manière d’un destructeur, pour corriger les pointeurs auto-référentiels vers le nouvel emplacement
- En Rust, ces pointeurs ne se trouvent pas forcément seulement « dans » la valeur déplacée ; ils peuvent aussi être, par exemple, dans un vecteur de pointeurs visant son propre état
- Pour tous les suivre, il faudrait au final une gestion mémoire à l’exécution proche d’un garbage collector
- Rust a choisi très tôt de ne pas avoir de move constructor, et beaucoup de code unsafe repose sur l’hypothèse qu’un déplacement de valeur peut se faire par simple copie mémoire
- Ajouter un move constructor plus tard constituerait un breaking change
-
offset pointer
- L’offset pointer consiste à compiler une auto-référence non pas en référence ordinaire, mais en décalage relatif à l’adresse de l’objet auto-référentiel
- Il n’est pas toujours possible de déterminer à la compilation quelle référence est auto-référentielle
- Selon les branches, une même valeur peut pointer tantôt à l’intérieur de son propre objet, tantôt à l’extérieur
- Pour gérer cela, il faudrait compiler les références sous une forme proche d’un enum offset/reference, ce qui a été jugé irréaliste au moment de la conception d’async/await
Les exigences du pinned typestate
- Une Future auto-référentielle n’a pas besoin d’être immuable au déplacement dès sa création ; elle doit pouvoir être déplacée librement jusqu’à un certain point de son cycle de vie, puis ne plus bouger ensuite
- Elle doit pouvoir être déplacée lorsqu’elle est combinée avec d’autres Future
- Une fois placée à son emplacement de poll, elle ne doit plus être déplacée
- Le modèle de Ralf Jung ajoute, aux typestates existants « owned » et « shared », un troisième état : le pinned typestate pour les Future auto-référentielles
- Une fois qu’un objet entre dans le pinned typestate, il ne doit plus jamais être déplacé
- Plus précisément, il ne faut pas invalider la mémoire de cet objet sans exécuter d’abord son destructeur
- En pratique, cela revient à exiger que l’objet ne soit pas déplacé vers un nouvel emplacement
- La plupart des types ne peuvent pas contenir d’auto-référence, si bien que le pinned typestate n’a pas de signification particulière pour eux
- Il est alors souhaitable que ces types échappent aux contraintes du pinning et puissent à nouveau être déplacés
- Le modèle formel détaillé du pinned typestate est présenté par Ralf Jung dans A Formal Look at Pinning
Pourquoi la conception ?Move a échoué
- Avant
Pin, une conception fondée sur un nouveau traitMovea été tentée- La plupart des types implémentent
Move - Les types susceptibles de contenir des auto-références n’implémentent pas
Move - Créer une référence vers une valeur d’un type qui n’implémente pas
Movefait entrer cette valeur dans le pinned typestate, empêchant tout déplacement ultérieur
- La plupart des types implémentent
- Cette approche paraissait intuitive, car elle liait la création de référence et la transition vers le pinning pour garantir la sûreté
- Elle a d’ailleurs été implémentée dans une branche du compilateur
- Sa limite fondamentale est qu’on peut vouloir référencer brièvement une valeur destinée à devenir auto-référentielle, sans encore vouloir la pinner
- Par exemple, on peut vouloir la stocker temporairement dans
Option, puis la récupérer avecOption::take
- Par exemple, on peut vouloir la stocker temporairement dans
- Le problème le plus important était toutefois la rétrocompatibilité
Movene pouvait pas être un auto trait- Des API stables existent déjà, comme
mem::swap, qui supposent qu’on peut toujours déplacer une valeur à partir d’une référence mutable
- L’ajout sous la forme
?Moven’était pas non plus rétrocompatible à cause des associated type- L’endroit où l’on ajoute une borne
?Traità un associated type est la définition du trait - Assouplir la borne d’un associated type existant peut casser du code qui dépendait de cette borne
- Le type future associé de
IntoFuture,TargetdeDerefMut, les types de retour de fonction, les éléments d’itérateur, les valeurs renvoyées par l’opérateur d’indexation ou les opérateurs arithmétiques, entre autres, sont fortement liés aux associated type
- L’endroit où l’on ajoute une borne
- Une edition n’aurait pas non plus facilement résolu le problème
- Pour que des crates de différentes editions puissent être combinées, les interfaces de trait doivent rester identiques
La conception de Pin
- La conception finale représente le pinned typestate non comme une propriété du type de l’objet, mais comme un état créé par un pointeur spécial
Pinest un wrapper autour d’un pointeur- Il peut encapsuler les types de référence built-in
- Il peut aussi encapsuler des smart pointers définis en bibliothèque, comme
Box
Pinplace dans le pinned typestate la cible pointée par ce pointeur, et cette cible ne doit plus être déplacée- Pour minimiser les changements, cette conception a été implémentée comme une API de bibliothèque plutôt que comme une fonctionnalité du compilateur
- Le code qui doit réellement modifier un objet pinné passe par des API unsafe
- Il doit alors garantir que l’objet ne sera pas déplacé via une référence mutable ordinaire
- Comme, pour la plupart des types, l’état pinné et l’état ordinaire ne diffèrent pas de manière significative, l’auto trait
Unpina été ajouté- Si un type ne peut pas être auto-référentiel, on peut obtenir une référence mutable depuis un pointeur pinné sans passer par unsafe
- Un objet qui implémente
Unpinpeut être déplacé hors dePinen toute sécurité
- Comme le pinning ne s’applique qu’aux pointeurs pinnés, les références ordinaires non pinnées continuent de fonctionner, y compris avec des types qui ne sont pas
Unpin - Des explications supplémentaires figurent dans la documentation standard du type
Pinet du modulepin - Le principal avantage de cette conception est qu’elle a pu être ajoutée sans casser le code existant
- Les API comme
swap, qui peuvent déplacer des données référencées, exigent une référence mutable - Une fois qu’un objet est pinné via
Pin, ces API ne peuvent plus lui être appliquées - Comme le pinned typestate ne s’applique qu’aux références pinnées spéciales, il ne remet pas en cause les garanties de rétrocompatibilité de l’ensemble du langage Rust
- Les API comme
Les problèmes d’ergonomie de Pin
Pinsatisfait les exigences de manière rétrocompatible, mais dès qu’un utilisateur doit le manipuler directement, on se heurte à un mur de complexité- Une première explication est que modifier un objet pinné nécessite du code unsafe
- Il ne faut toutefois pas exagérer ce point
Pin::setpermet d’affecter en toute sécurité un objet pinné- En pratique, le code qui doit réellement modifier un objet pinné est le plus souvent du code généré par le compilateur lorsqu’il abaisse une fonction async en Future ; les cas où l’utilisateur l’écrit à la main sont rares
- Dire que
Pinest difficile parce qu’il fonctionne de manière conditionnelle ne touche pas non plus la cause profonde- Rust possède d’autres fonctionnalités dont le comportement dépend du contexte tout en restant relativement compréhensibles
- Les non-lexical lifetimes en sont un exemple : elles permettent à une lifetime de se terminer à des points différents selon les branches
- Le vrai problème est que
Pinest un pur type de bibliothèque, alors que les types de référence ordinaires sont intégrés au langage et bénéficient d’un grand nombre de syntaxes et de sugars- Des opérations naturelles avec une référence ordinaire disparaissent avec une référence pinnée
- Le modèle mental que les utilisateurs construisent à partir du comportement des références acceptées par le compilateur s’effondre avec les références pinnées
Reborrowing et Pin::as_mut
- Une référence mutable ordinaire
&mut Tn’implémente pasCopy, mais peut être passée plusieurs fois comme argument- Le compilateur effectue implicitement un reborrowing, comme s’il insérait
&mut *xà la place dex
- Le compilateur effectue implicitement un reborrowing, comme s’il insérait
Pin<&mut T>est un type de bibliothèque ordinaire qui n’implémente pasCopy, et ne bénéficie donc pas de cette commodité- Si l’on utilise
Pin<&mut T>plus d’une fois, on peut obtenir une erreur d’utilisation après move, ou une erreur de lifetime encore plus difficile à comprendre - Il faut reborrow explicitement en appelant
Pin::as_mut
- Si l’on utilise
- Avec une référence mutable ordinaire, on peut affecter directement via la déréférence et l’opérateur d’affectation ; avec
Pin, il faut apprendre la méthodeset- Cette multiplication d’API spéciales vient du fait que
Pinest un type de bibliothèque sans support syntaxique du langage
- Cette multiplication d’API spéciales vient du fait que
Pinned projection et Drop
- La pinned projection est le problème qui consiste à obtenir, à partir d’une référence pinnée sur un objet, une référence pinnée sur l’un de ses champs
- Ici, projection désigne l’accès d’un objet vers l’un de ses champs
- Comme c’est bien plus difficile qu’un simple accès de champ sur une référence ordinaire, on utilise des crates tierces comme pin-project-lite
- Ces crates imposent d’apprendre une nouvelle API complexe, souvent avec des macros
- L’interaction la plus problématique survient entre pinned projection et le trait
DropDrop::dropreçoit une référence mutable ordinaire- Si un type possède un champ auto-référentiel, qu’on fait une pin projection vers ce champ pour le poll, puis qu’on déplace ce champ dans le destructeur, on peut violer les garanties du pinning
- Par exemple, si l’on pinne ensuite cette future sur la stack dans le destructeur avant de la poll, on enfreint les garanties de pinning précédentes
- Des crates comme
pin-project-litetraitent ce problème en limitant la manière de définir un destructeur- En pratique, cela fonctionne, mais cela ajoute une couche de complexité documentaire lorsqu’il faut expliquer les garanties du pinning
Dropa été stabilisé avantPin, d’où la nécessité de ce contournement
Évaluation actuelle et pistes d’amélioration
Pina permis de compiler des fonctions async contenant des références arbitraires en objets auto-référentiels sûrs- Or les références sont un élément central de la façon dont les utilisateurs de Rust écrivent leur code ; sans cela, l’ergonomie d’async/await aurait été bien plus faible
- Dans le même temps,
Pina été ajouté d’une manière totalement rétrocompatible avec le Rust existant Pinest devenu un composant fondamental de l’écosystème qui soutient les services réseau haute performance et d’autres usages de la programmation asynchrone- Mais manipuler des références pinnées reste bien plus difficile que manipuler des références ordinaires, et
Pincrée effectivement un mur de complexité - Le concept clé de la prochaine direction d’amélioration est celui des pinned places
1 commentaires
Avis sur Hacker News
J’ai toujours trouvé que Pin était difficile à comprendre, car la documentation officielle ne l’explique pas clairement
On voit notamment beaucoup d’explications du type « Pin garantit qu’un objet ne sera jamais déplacé », mais ce n’est pas vrai
Ce n’est exact que lorsque l’objet n’est pas
Unpin, et comme la plupart des objets ordinaires sontUnpin, Pin ne fait généralement rienIl m’a fallu très longtemps pour comprendre cela, et je trouve que l’ensemble des types
Tpour lesquels Pin a réellement un sens est assez particulier et étrange, ce que la documentation ne souligne pas suffisammentBien sûr, les types que l’on manipulera réellement comme épinglés — les futures et les streams — ont beaucoup plus de chances d’être ce genre d’objets particuliers
Cela dit, je pense que la documentation s’est beaucoup améliorée au fil des années
Quand je l’ai consultée en écrivant ce billet, j’ai été surpris de la voir se concentrer sur des points assez pertinents ; dans mon souvenir, vers 2019, elle penchait bien davantage vers une spécification de contrat qui aurait plutôt eu sa place dans la référence Rust que dans la documentation d’API de
stdÀ mon avis, si les utilisateurs ont du mal avec Pin, c’est parce que Pin n’a pas de signification à lui seul
C’est différent des autres wrappers du langage ; la seule exception serait peut-être
AssertUnwindSafe, que quasiment personne n’utilise pour son usage d’origineQuand on a un
Pin<&mut InnerType>, Pin, pris seul dans le langage ou la bibliothèque standard, ne dit pas ce que l’on peut ou ne peut pas faireEn revanche, si
InnerTypese déclareUnpin, cela signifie que l’on peut faire tout ce qu’un pointeur ordinaire permet de faireÀ la place, Pin fonctionne sur le mode « apportez vous-même la sémantique » : le fournisseur de
InnerTypeajoute des méthodes et des API supplémentaires,unsafeen interne, pour manipuler en toute sûreté des objets épinglésLe but de Pin lui-même est de fournir un pointeur doté de moins de capacités intrinsèques, par exemple remplacer via
&mutou extraire et déplacer depuis uneBox, afin que le type interne puisse autoriser en toute sûreté des capacités supplémentaires par-dessusJe pense que cette ambiguïté de sens est ce qui déroute le plus les gens, et il m’a aussi fallu pas mal de temps pour la comprendre
Les notions de champs structurels et non structurels ne sont qu’un dispositif permettant des schémas d’accès courants, du type « ce champ est une donnée ordinaire, mais celui-là contient un objet qui veut lui-même être épinglé »
Unpin, cela veut dire que la cible de ce pointeur ne pourra plus jamais être déplacéePlus précisément, cela signifie que l’on ne peut pas invalider la cible sans exécuter son destructeur, et c’est justement pour cela que le déplacement pose problème
En renonçant à certains droits, on en obtient d’autres, comme le droit de stocker des valeurs auto-référentielles
Les contrats entre composants fonctionnent généralement de cette manière
De même, si l’on renonce au droit de modifier via une référence, on peut en même temps permettre à cette référence d’avoir des alias
Chaque fois que j’y pense, même si c’est un sujet complètement différent et beaucoup plus lourd, cela me rappelle cette réplique du film Lincoln : « Si nous obéissons à la loi, Alex, si nous lui obéissons jusqu’à perdre des libertés — par exemple la liberté d’opprimer —, nous découvrirons peut-être d’autres libertés que nous ne connaissions pas auparavant »
Cela dit, je suis d’accord pour dire que le fait de ne pas pouvoir utiliser directement ces droits dans du code sûr pose un problème pédagogique
Car il est difficile de montrer simplement ce que l’on peut faire avec une référence épinglée, à part « appeler la méthode
pollgénérée par le compilateur »Je développe professionnellement en Rust depuis plusieurs années, mais honnêtement, je ne comprends pas si bien Pin que ça
Je connais la théorie, mais je n’ai pas vraiment d’intuition sur le moment où il faut l’utiliser
En pratique, l’usage de Pin ressemble surtout à « j’ai essayé quelque chose, le compilateur s’est plaint, j’ai épinglé ceci et cela, et ça compile »
Dans le code que j’écris au quotidien, cela n’a encore jamais été un obstacle au point de devoir vraiment m’asseoir pour le comprendre en profondeur
unsafeet soyons reconnaissants envers les gens très intelligents du compilateur qui ont déjà tout réglé »À l’inverse, en C++, je me retrouvais souvent à marcher au bord du marécage des « choses que je ne comprends pas mais que je dois absolument utiliser », avant de me faire dévorer par un crocodile
Quand on l’enseigne, pour bien faire comprendre que les éléments
Unpinne sont pas affectés parPin, on pourrait utiliser une analogie du monde réel avec un outil conçu pour maintenir quelque chose en place mais qui n’a aucun effet dans certains casLes crochets de velcro n’adhèrent pas aux surfaces lisses :
Pin→ velcro,Unpin→ surface lisseLes aimants n’affectent pas les matériaux non magnétiques :
Pin→ aimant,Unpin→ non magnétique/verre/laitonLa colle n’adhère pas aux surfaces antiadhésives :
Pin→ colle,Unpin→ antiadhésifAinsi, on comprend clairement que le « velcro » fixe un objet en place, mais que si l’objet est « lisse », il n’est pas affecté par le mécanisme du velcro
Vu l’ambiance des choix de noms dans l’écosystème Rust, cela aurait été élégant de renommer le trait dans une direction du type aimant et non magnétique
Il me semble que
Unpinsignifie qu’un objet est prêt à être épinglé à tout momentJ’ai lu l’article hier soir, mais j’ai déjà oublié si l’épinglage nécessite une étape de correction
Du coup, je vois plutôt
T: Pin + !Unpincomme une feuille de papier que l’on ne peut fixer qu’avec des agrafes, etT: Pin + Unpincomme un tableau muni d’un crochet, que l’on peut suspendre à un clou puis décrocher sans abîmer le crochetLe terme « identité de valeur » n’est défini nulle part dans cet article, et je ne l’ai pas trouvé non plus dans la documentation de Mojo ; la raison pour laquelle Modular affirme que Mojo résout le problème que Pin cherche à résoudre n’est donc pas claire.
Je ne prétends pas non plus connaître la réponse, mais cela me fait penser à l’excellente présentation de Dave Abrahams, qui a travaillé avec Chris Lattner sur la sémantique de valeur de Swift.
Le titre de la présentation est « Value Semantics: Safety, Independence, Projection, & Future of Programming ».
[0] https://www.youtube.com/watch?v=QthAU-t3PQ4
Rust dispose également de références comme types de première classe, tandis que Swift et, à mon avis, Mojo n’autorisent les références que comme mode de passage de paramètres.
Mojo semble avoir étendu les paramètres
inoutde Swift pour inclure aussi un mode de passage par référence immuable.Empêcher de stocker des références dans un objet résout effectivement le problème des « structures auto-référentielles », puisqu’il devient impossible d’implémenter le genre de code que Rust compile.
Mais le paragraphe cité ne dit absolument pas cela à propos de Mojo, donc je trouve assez confus de comprendre ce qu’il veut dire.
À mon avis, le problème tient au fait que, lorsqu’on a une référence
&mutvers une valeur, on peut déplacer cette valeur avec quelque chose commemem::swap/replace.Mais il est rare d’avoir réellement besoin de le faire.
Si cela n’avait pas été autorisé, il me semble qu’avoir une référence
&mutvers une valeur auto-référentielle aurait été parfaitement sûr.Il aurait peut-être pu exister un moyen d’opter explicitement pour le déplacement via une référence uniquement quand c’est nécessaire, et si
swapetreplaceavaient été rendusunsafe, tout ce problème aurait peut-être pu être évité.J’aimerais que quelqu’un explore cet espace de conception.
&mutétait trop puissant.Si
&mutn’avait pas donné le droit de déplacer la valeur qu’il contient, toute la conception aurait été bien plus simple.J’aborderai ce point dans le prochain article.
Rust doit préserver la compatibilité ascendante et a déjà décidé qu’il était possible de déplacer des valeurs depuis
&mut, mais si l’on n’était pas lié par les décisions passées, une conception beaucoup plus propre serait clairement possible.mem::swapn’est qu’une des manières de déplacer une valeur via une référence mutable, et il y en a beaucoup d’autres.Option::takeest un exemple que j’utilise assez souvent, et ce serait vraiment étrange qu’il soitunsafe.J’aime bien ce contexte historique. WithoutBoats a déjà suscité beaucoup de discussions très actuelles sur les itérateurs asynchrones,
polletpin.https://news.ycombinator.com/from?site=without.boats
Je ne crois pas qu’il y ait beaucoup de communautés qui explorent publiquement avec autant de profondeur les détails internes d’un langage, et c’est très intéressant à suivre.
L’asynchrone est encore à moitié inachevé et très complexe.
Je dis cela en écrivant du code Rust 40 heures par semaine depuis trois ans.
On peut imaginer un langage proche de Rust qui aurait des constructeurs de déplacement, où tous les sous-types de
Futurecréés seraient opaques et automatiquement alloués sur le tasDans ce cas, l’utilisateur n’aurait aucun moyen de les détruire et, comme ils seraient opaques et situés ailleurs sur le tas, aucun moyen de les déplacer non plus, ce qui pourrait rendre Pin inutile
Car le fait d’avoir un constructeur de déplacement signifie que, conceptuellement, un déplacement est une destruction suivie d’une recréation
Cela a l’effet bénéfique de permettre de fusionner et d’inliner les
Futureavant leur exécutionC’est aussi similaire à l’immutabilité en Rust. Il n’y a pas de mémoire immuable, seulement des références immuables
Mais cela entraînerait une allocation séparée à chaque appel de fonction asynchrone, ce qui est très mauvais pour la localité mémoire
Une forme de pile virtuelle serait bien meilleure que cela, mais pour optimiser les piles afin qu’elles soient petites par défaut, il faudrait au final un garbage collector
stdun traitMoveintégré au langage, à un niveau comparable àCopyMovedéfinirait une fonction qui déplace une valeur d’une adresse mémoire à une autre, et les structs sansimpl Movene pourraient pas être déplacéesPresque tous les types recevraient
#[derive(Move)], ce qui n’aurait qu’à implémenter une simple fonction de déplacement copiant les octetsMais cela ouvrirait la voie aux types auto-référentiels, aux futures, et à beaucoup d’autres choses nécessitant des comportements de déplacement plus complexes
En pratique, il serait peut-être plus logique de le diviser en deux traits, reflétant la différence entre
CopyetCloneL’un serait un trait marqueur indiquant au compilateur qu’il peut simplement déplacer les octets, et l’autre autoriserait l’implémentation d’un « constructeur de déplacement » défini par l’utilisateur
Pin est tellement difficile à comprendre que j’aimerais qu’on ait Move
Un concept complexe y est enveloppé dans des doubles négations, parfois des triples négations. Quand on voit quelque chose comme
fn(...), on se dit « c’est quoi ça ? », et arrivé aux projections pinunsafe, on décrocheJe ne sais pas quand c’est sûr ni quand ça ne l’est pas, et je finis simplement par laisser tomber
Passer d’un Rust sans
Moveà un Rust avecMoveserait pénibleIl faudrait ajouter
#[derive(Move)]à presque toutes les structs écrites jusqu’ici, etstdaussi devrait le fairePour tous les types non épinglés des éditions existantes, le compilateur devrait inférer une implémentation du trait
MoveCe serait mécaniquement possible, mais ce serait juste beaucoup de travail
Le Rust asynchrone est affreux. C’est particulièrement vrai quand on le compare aux futures/promises de presque tous les autres langages
Un jour, quelqu’un améliorera le modèle de sûreté mémoire de Rust et créera un nouveau langage système proche de Rust, avec un trait
Moveet de meilleures futuresPersonnellement, j’aimerais aussi avoir de l’exécution à la compilation à la place du système de macros de Rust
J’aime Rust, et j’aime aussi tout le travail que l’équipe a accompli pendant des années
Mais le langage que j’attends vraiment, c’est celui qui viendra après Rust
Un langage avec les mêmes idées, mais qui aura appris des erreurs de Rust, et l’on voit de plus en plus clairement à quoi pourrait ressembler un meilleur langage de cette famille
J’ai vraiment hâte
Encore un excellent article de WithoutBoats
Honnêtement, c’est l’un des aspects de Rust pour lesquels je suis content qu’il soit enfoui derrière les abstractions internes des runtimes asynchrones
Cela dit, en dehors des implémentations personnalisées de
Future, je me demande où Pin est réellement utiliséÀ en juger par la formulation, on pourrait imaginer l’utiliser dans du FFI
Par exemple, si une fonction
externrenvoie un*mut Tet reçoit un pointeur, on pourrait l’envelopper dansPin<&mut T>pour lui donner une meilleure sémantiqueMais l’article dit : « Un autre fait concernant l’état épinglé d’un type est qu’il est complètement sans rapport avec la plupart des types. Si la valeur d’un type ne peut jamais contenir d’auto-référence, l’épingler ne sert à rien »
Je suis encore très débutant en FFI, donc j’aimerais comprendre quelle est la meilleure façon de l’envelopper dans du Rust sûr
Cela vaut aussi lorsqu’on interagit avec des types système qui dépendent de leur adresse
Par exemple, pour certains mutex/futex de systèmes d’exploitation, la documentation du noyau indique que l’objet de verrouillage en espace utilisateur ne doit pas changer d’adresse après son initialisation ; il me semble donc que
stdutilise quelque chose d’équivalent à PinCe qui est particulier, c’est que l’adresse ne doit pas changer même lorsque le verrou n’est pas pris
En général, la contrainte ne vaut que pendant que le verrou est pris, et dans ce cas, comme on ne peut pas déplacer l’objet visé par une référence active, Pin n’est pas nécessaire
On a l’impression qu’un énorme travail est fait pour éviter de corriger le vrai problème : l’inefficacité des threads.
Tout le code asynchrone, absolument tout, sans exception, est un hack qui implémente des threads légers avec beaucoup de sucre syntaxique pour la gestion d’état.
Dans des langages comme Rust, cela ajoute énormément de complexité qui ne devrait pas exister à l’origine.
Si l’on corrigeait les problèmes d’efficacité et de scalabilité des threads, tout cela disparaîtrait.
Pouf, disparu.
C’est comparable au
nulldans des langages comme Java, cette « erreur à mille milliards de dollars ».Une seule décision de conception — ou ici, une absence de conception — engendre une complexité énorme.
L’annulation est très utile dans les applications réseau et les GUI.
Les threads rendent difficile le fait d’exploiter pleinement à la fois le CPU et le réseau, sans monopoliser excessivement l’un ou l’autre.
Dès qu’on commence à faire passer du travail d’un pool de threads à un autre, on s’engage sur la voie qui consiste à réimplémenter les futures.
Sinon, on finit par travailler avec des callbacks/événements, le code se fragmente, et
async/awaitest précisément le sucre syntaxique qui devait remédier à cela.L’alternative à l’annulation et aux timeouts consiste, comme en Go, à faire circuler un objet
Contextdans tout le code, ce qui pose le problème du code de bas niveau qui appelle naïvement des fonctions ne respectant pas correctement leContext.Ce n’est qu’à peine mieux que le problème des fonctions non asynchrones au sein de code asynchrone.
Je me demande donc concrètement ce qu’il aurait fallu faire.
Fallait-il simplement lever les bras au ciel et dire : « un jour, quelqu’un corrigera peut-être Linux pour rendre les threads magiquement rapides, donc nous n’ajouterons pas l’asynchrone à notre langage » ?
De plus, faire du système d’exploitation le scheduler de toutes les tâches asynchrones obligerait tous les runtimes à utiliser le scheduler de l’OS, rendant impossibles divers designs de scheduler.
Même après qu’il est apparu que « l’approche privilégiée » était impraticable, il semble qu’aucune réévaluation coût/bénéfice ne soit faite.
« Nous voulons la fonctionnalité X, peu importe les conséquences » est rarement une stratégie gagnante dans la conception d’un langage.