Rust: [Stabilisation] async/attente MVP

Créé le 26 juin 2019  ·  58Commentaires  ·  Source: rust-lang/rust

Objectif de stabilisation : 1,38.0 (bêta coupé 2019-08-15)

Résumé

Il s'agit d'une proposition visant à stabiliser une fonctionnalité asynchrone/attente minimale viable, qui comprend :

  • async annotations sur les fonctions et les blocs, les obligeant à être retardés dans l'évaluation et à évaluer à la place dans un futur.
  • Un opérateur await , valide uniquement dans un contexte async , qui prend un futur comme argument et fait que le futur extérieur dans lequel il se trouve cède le contrôle jusqu'à ce que le futur attendu soit terminé.

Discussions précédentes liées

RFC :

Problèmes de suivi :

Stabilisations :

Décisions majeures prises

  • Le futur auquel une expression asynchrone évalue est construit à partir de son état initial, n'exécutant aucun code du corps avant de céder.
  • La syntaxe des fonctions asynchrones utilise le type de retour "interne" (le type qui correspond à l'expression interne return ) plutôt que le type de retour "externe" (le futur type auquel un appel à la fonction évalue)
  • La syntaxe de l'opérateur d'attente est la "syntaxe de point postfixe", expression.await , par opposition à la syntaxe plus courante await expression ou à une autre syntaxe alternative.

Travaux de mise en œuvre bloquant la stabilisation

  • [x] async fns devrait pouvoir accepter plusieurs durées de vie #56238
  • [x] la taille des générateurs ne devrait pas croître de façon exponentielle #52924
  • [ ] Documentation viable minimale pour la fonctionnalité async/wait
  • [ ] Tests suffisants du compilateur du comportement

Travail futur

  • Async/wait dans des contextes non standard : async et await dépendent actuellement de TLS pour fonctionner. Il s'agit d'un problème de mise en œuvre qui ne fait pas partie de la conception, et bien qu'il ne bloque pas la stabilisation, il est destiné à être résolu à terme.
  • Fonctions asynchrones d'ordre supérieur : async tant que modificateur pour les littéraux de fermeture n'est pas stabilisé ici. Plus de travail de conception est nécessaire concernant la capture et l'abstraction sur les fermetures asynchrones avec des durées de vie.
  • Méthodes de traits asynchrones : cela implique un travail de conception et de mise en œuvre important, mais il s'agit d'une fonctionnalité hautement souhaitable.
  • Traitement de flux : la paire avec le trait Future dans la bibliothèque Futures est le trait Stream, un itérateur asynchrone. L'intégration de la prise en charge de la manipulation des flux dans std et le langage est une fonctionnalité souhaitable à long terme.
  • Optimisation des représentations des générateurs : Davantage de travail peut être fait pour optimiser la représentation des générateurs afin de les rendre plus parfaitement dimensionnés. Nous nous sommes assurés qu'il s'agit strictement d'un problème d'optimisation et qu'il n'est pas sémantiquement significatif.

Fond

La gestion des E/S non bloquantes est très importante pour développer des services réseau hautes performances, un cas d'utilisation cible pour Rust avec un intérêt significatif de la part des utilisateurs de production. Pour cette raison, une solution permettant de rendre ergonomique et réalisable l'écriture de services utilisant des E/S non bloquantes est depuis longtemps un objectif de Rust. La fonction async/wait est le point culminant de cet effort.

Avant la version 1.0, Rust disposait d'un système de threads verts, dans lequel Rust fournissait une primitive de threads alternative au niveau du langage construite au-dessus des E/S non bloquantes. Cependant, ce système a causé plusieurs problèmes : le plus important, l'introduction d'un environnement d'exécution de langage qui a eu un impact sur les performances même des programmes qui ne l'utilisaient pas, a considérablement augmenté la surcharge de FFI et plusieurs problèmes de conception majeurs non résolus liés à la mise en œuvre des piles greenthread. .

Après la suppression des greenthreads, les membres du projet Rust ont commencé à travailler sur une solution alternative basée sur l'abstraction des futurs. Parfois aussi appelés promesses, les futures avaient très bien réussi dans d'autres langages en tant qu'abstraction basée sur une bibliothèque pour les E/S non bloquantes, et on savait qu'à long terme, ils correspondaient bien à une syntaxe async/wait qui ne pouvait les rendre que légèrement moins pratiques que un système de greenthreading totalement invisible.

La percée majeure dans le développement de l'abstraction Future a été l'introduction d'un modèle basé sur des sondages pour les futures. Alors que d'autres langages utilisent un modèle basé sur le rappel, dans lequel le futur lui-même est responsable de la planification de l'exécution du rappel lorsqu'il est terminé, Rust utilise un modèle basé sur le sondage, dans lequel un exécuteur est chargé de sonder le futur jusqu'à son terme, et le future informant simplement l'exécuteur qu'il est prêt à faire de nouveaux progrès en utilisant l'abstraction Waker. Ce modèle a bien fonctionné pour plusieurs raisons :

  • Cela a permis à rustc de compiler des futures sur des machines à états qui avaient la surcharge de mémoire la plus minimale, à la fois en termes de taille et d'indirection. Cela présente des avantages significatifs en termes de performances par rapport à l'approche basée sur le rappel.
  • Il permet à des composants tels que l'exécuteur et le réacteur d'exister en tant qu'API de bibliothèque, plutôt qu'en tant que partie du langage d'exécution. Cela évite d'introduire des coûts globaux qui ont un impact sur les utilisateurs qui n'utilisent pas cette fonctionnalité et permet aux utilisateurs de remplacer facilement les composants individuels de leur système d'exécution, plutôt que de nous obliger à prendre une décision de boîte noire pour eux au niveau de la langue.
  • Il crée également toutes les bibliothèques de primitives de concurrence, plutôt que d'intégrer la concurrence dans le langage via la sémantique des opérateurs async et wait. Cela rend la concurrence plus claire et plus visible à travers le texte source, qui doit utiliser une primitive de concurrence identifiable pour introduire la concurrence.
  • Il permet une annulation sans frais généraux, en permettant d'abandonner l'exécution des contrats à terme avant qu'ils ne soient terminés. Rendre tous les contrats à terme annulables gratuitement présente des avantages en termes de performances et de clarté du code pour les exécuteurs et les primitives de concurrence.

(Les deux derniers points ont également été identifiés comme une source de confusion pour les utilisateurs venant d'autres langages dans lesquels ils ne sont pas vrais, et apportant avec eux des attentes de ces langages. Cependant, ces propriétés sont toutes deux des propriétés inévitables du modèle basé sur les sondages qui a d'autres avantages évidents et sont, à notre avis, des propriétés bénéfiques une fois que les utilisateurs les comprennent.)

Cependant, le modèle basé sur les sondages souffrait de graves problèmes d'ergonomie lorsqu'il interagissait avec des références ; essentiellement, les références à travers les points de rendement ont introduit des erreurs de compilation insolubles, même si elles devraient être sûres. Cela a abouti à un code complexe et bruyant plein d'arcs, de mutex et de fermetures de mouvement, dont aucun n'était strictement nécessaire. Même en mettant ce problème de côté, sans primitif de niveau de langue, les futurs ont souffert de forcer les utilisateurs à écrire des rappels hautement imbriqués.

Pour cette raison, nous avons poursuivi le sucre syntaxique async/wait avec la prise en charge de l'utilisation normale des références à travers les points de rendement. Après avoir introduit l'abstraction Pin qui a permis de prendre en charge les références à travers les points de rendement en toute sécurité, nous avons développé une syntaxe native async/wait qui compile des fonctions dans nos futurs basés sur les sondages, permettant aux utilisateurs d'obtenir les avantages en termes de performances des E/S asynchrones avec futures en écrivant un code très similaire au code impératif standard. Cette dernière caractéristique fait l'objet de ce rapport de stabilisation.

description de la fonctionnalité async/attente

Le modificateur async

Le mot-clé async peut être appliqué à deux endroits :

  • Avant une expression de bloc.
  • Avant une fonction libre ou une fonction associée dans une impl inhérente.

_(D'autres emplacements pour les fonctions asynchrones - les littéraux de fermeture et les méthodes de trait, par exemple, seront développés et stabilisés à l'avenir.)_

Le modificateur async ajuste l'élément qu'il modifie en "le transformant en un futur". Dans le cas d'un bloc, le bloc est évalué à un futur de son résultat, plutôt qu'à son résultat. Dans le cas d'une fonction, les appels à cette fonction renvoient un futur de sa valeur de retour, plutôt que sa valeur de retour. Le code à l'intérieur d'un élément modifié par un modificateur async est considéré comme étant dans un contexte async.

Le modificateur async effectue cette modification en amenant l'élément à être évalué à la place comme un pur constructeur d'un futur, en prenant les arguments et les captures comme champs du futur. Chaque point d'attente est traité comme une variante distincte de cette machine à états, et la méthode "sondage" du futur fait avancer le futur à travers ces états en fonction d'une transformation du code que l'utilisateur a écrit, jusqu'à ce qu'il atteigne finalement son état final.

Le modificateur async move

Semblable aux fermetures, les blocs asynchrones peuvent capturer des variables dans la portée environnante dans l'état du futur. Comme les fermetures, ces variables sont par défaut capturées par référence. Cependant, ils peuvent à la place être capturés par valeur, en utilisant le modificateur move (tout comme les fermetures). async vient avant move , ce qui fait de ces blocs des blocs async move { } .

L'opérateur await

Dans un contexte asynchrone, une nouvelle expression peut être formée en combinant une expression avec l'opérateur await , en utilisant cette syntaxe :

expression.await

L'opérateur wait ne peut être utilisé que dans un contexte asynchrone, et le type d'expression auquel il est appliqué doit implémenter le trait Future . L'expression d'attente est évaluée à la valeur de sortie du futur auquel elle est appliquée.

L'opérateur d'attente donne le contrôle du futur auquel le contexte asynchrone évalue jusqu'à ce que le futur auquel il est appliqué soit terminé. Cette opération de céder le contrôle ne peut pas être écrite dans la syntaxe de surface, mais si elle le pouvait (en utilisant la syntaxe YIELD_CONTROL! dans cet exemple), la suppression de wait ressemblerait à peu près à ceci :

loop {
    match $future.poll(&waker) {
        Poll::Ready(value)  => break value,
        Poll::Pending       => YIELD_CONTROL!,
    }
}

Cela vous permet d'attendre que les futures finissent de s'évaluer dans un contexte asynchrone, en transmettant le rendement du contrôle via Poll::Pending vers le contexte asynchrone le plus externe, en fin de compte vers l'exécuteur sur lequel le futur a été engendré.

Principaux points de décision

Cédant immédiatement

Nos fonctions et blocs asynchrones « rendent immédiatement » - les construire est une fonction pure qui les met dans un état initial avant d'exécuter le code dans le corps du contexte asynchrone. Aucun du code du corps n'est exécuté jusqu'à ce que vous commenciez à interroger ce futur.

Ceci est différent de beaucoup d'autres langages, dans lesquels les appels à une fonction asynchrone déclenchent le travail pour commencer immédiatement. Dans ces autres langages, async est une construction intrinsèquement concurrente : lorsque vous appelez une fonction asynchrone, elle déclenche une autre tâche pour commencer à s'exécuter simultanément avec votre tâche actuelle. Dans Rust, cependant, les contrats à terme ne sont pas intrinsèquement exécutés de manière concurrente.

Nous pourrions avoir des éléments asynchrones exécutés jusqu'au premier point d'attente lorsqu'ils sont construits, au lieu de les rendre purs. Cependant, nous avons décidé que c'était plus déroutant : que le code soit exécuté lors de la construction du futur ou de l'interrogation, cela dépendrait du placement de la première attente dans le corps. Il est plus simple de raisonner pour que tout le code soit exécuté pendant l'interrogation, et jamais pendant la construction.

Référence:

Syntaxe du type de retour

La syntaxe de nos fonctions asynchrones utilise le type de retour "interne", plutôt que le type de retour "externe". C'est-à-dire qu'ils disent qu'ils renvoient le type auquel ils finissent par évaluer, plutôt que de dire qu'ils renvoient un futur de ce type.

À un certain niveau, il s'agit de décider du type de clarté à privilégier : comme la signature inclut également l'annotation async , le fait qu'elles renvoient un futur est rendu explicite dans la signature. Cependant, il peut être utile pour les utilisateurs de voir que la fonction renvoie un futur sans avoir également à remarquer le mot-clé async. Mais cela ressemble aussi à du passe-partout, puisque l'information est également véhiculée par le mot-clé async .

Ce qui a vraiment fait pencher la balance pour nous, c'est la question de l'élision à vie. Le type de retour "externe" de toute fonction asynchrone est impl Future<Output = T> , où T est le type de retour interne. Cependant, ce futur capture également les durées de vie de tous les arguments d'entrée en soi : c'est l'opposé de la valeur par défaut pour impl Trait, qui n'est supposé capturer aucune durée de vie d'entrée à moins que vous ne les spécifiiez. En d'autres termes, l'utilisation du type de retour externe signifierait que les fonctions asynchrones n'ont jamais bénéficié de l'élision à vie (à moins que nous ayons fait quelque chose d'encore plus inhabituel, comme faire fonctionner les règles d'élision à vie différemment pour les fonctions asynchrones et d'autres fonctions).

Nous avons décidé qu'étant donné à quel point le type de retour externe serait verbeux et franchement déroutant à écrire, cela ne valait pas la peine de signaler en plus que cela renvoie un futur pour obliger les utilisateurs à l'écrire.

Commande de destructeur

L'ordre des destructeurs dans les contextes asynchrones est le même que dans les contextes non asynchrones. Les règles exactes sont un peu compliquées et hors de portée ici, mais en général, les valeurs sont détruites lorsqu'elles sortent de la portée. Cela signifie, cependant, qu'ils continuent d'exister pendant un certain temps après leur utilisation jusqu'à ce qu'ils soient nettoyés. Si ce temps inclut des instructions d'attente, ces éléments doivent être conservés dans l'état futur afin que leurs destructeurs puissent être exécutés au moment approprié.

Nous pourrions, en tant qu'optimisation de la taille des états futurs, réorganiser les destructeurs pour qu'ils soient plus tôt dans certains ou tous les contextes (par exemple, les arguments de fonction inutilisés pourraient être supprimés immédiatement, au lieu d'être stockés dans l'état futur). Cependant, nous avons décidé de ne pas le faire. L'ordre des destructeurs peut être un problème épineux et déroutant pour les utilisateurs, et est parfois très important pour la sémantique du programme. Nous avons choisi de renoncer à cette optimisation au profit de la garantie d'un ordre de destructeur aussi simple que possible - le même ordre de destructeur si tous les mots-clés async et wait étaient supprimés.

(Un jour, nous pourrions être intéressés par des moyens de marquer les destructeurs comme purs et réordonnables. Il s'agit d'un futur travail de conception qui a également des implications sans rapport avec async/wait.)

Référence:

Attendre la syntaxe de l'opérateur

Un écart majeur par rapport aux fonctionnalités async/wait des autres langages est la syntaxe de notre opérateur d'attente. Cela a fait l'objet d'une énorme quantité de discussions, plus que toute autre décision que nous avons prise dans la conception de Rust.

Depuis 2015, Rust dispose d'un opérateur suffixe ? pour la gestion ergonomique des erreurs. Depuis bien avant la version 1.0, Rust a également eu un opérateur suffixe . pour l'accès aux champs et les appels de méthode. Étant donné que le cas d'utilisation principal des contrats à terme consiste à effectuer une sorte d'OI, la grande majorité des contrats à terme évaluent à un Result avec certains
sorte d'erreur. Cela signifie qu'en pratique, presque toutes les opérations d'attente sont séquencées avec un ? ou un appel de méthode après. Étant donné la priorité standard des opérateurs de préfixe et de suffixe, presque tous les opérateurs d'attente auraient été écrits (await future)? , ce que nous avons considéré comme très peu ergonomique.

Nous avons donc décidé d'utiliser une syntaxe suffixe, qui compose très bien avec les opérateurs ? et . . Après avoir examiné de nombreuses options syntaxiques différentes, nous avons choisi d'utiliser l'opérateur . suivi du mot-clé wait.

Référence:

Prise en charge des exécuteurs simples et multithreads

Rust est conçu pour faciliter l'écriture de programmes simultanés et parallèles sans imposer de coûts aux personnes qui écrivent des programmes qui s'exécutent sur un seul thread. Il est important de pouvoir exécuter des fonctions asynchrones à la fois sur des exécuteurs monothread et des exécuteurs multithread. La principale différence entre ces deux cas d'utilisation est que les exécuteurs multithreads limiteront les futurs qu'ils peuvent générer de Send , contrairement aux exécuteurs monothreads.

Semblable au comportement existant de la syntaxe impl Trait , les fonctions asynchrones "fuient" les traits automatiques du futur qu'elles renvoient. C'est-à-dire qu'en plus d'observer que le type de retour externe est un futur, l'appelant peut également observer si ce type est Send ou Sync, sur la base d'un examen de son corps. Cela signifie que lorsque le type de retour d'un fn asynchrone est planifié sur un exécuteur multithread, il peut vérifier si cela est sûr ou non. Cependant, il n'est pas nécessaire que le type soit Send, et les utilisateurs sur des exécuteurs monothreads peuvent donc tirer parti de primitives monothread plus performantes.

Certains craignaient que cela ne fonctionne pas bien lorsque les fonctions asynchrones étaient étendues aux méthodes, mais après discussion, il a été déterminé que la situation ne serait pas significativement différente.

Référence:

Bloqueurs de stabilisation connus

Taille de l'état

Numéro : #52924

La façon dont la transformation asynchrone vers une machine à états est actuellement mise en œuvre n'est pas du tout optimale, ce qui fait que l'état devient beaucoup plus grand que nécessaire. Il est possible, parce que la taille de l'état augmente de manière superlinéaire, de déclencher des débordements de pile sur la pile réelle lorsque la taille de l'état augmente plus que la taille d'un thread système normal. Améliorer ce codegen afin que la taille soit plus raisonnable, du moins pas assez mauvaise pour provoquer des débordements de pile en utilisation normale, est une correction de bogue bloquant.

Durées de vie multiples dans les fonctions asynchrones

Numéro : 56238

les fonctions asynchrones devraient pouvoir avoir plusieurs durées de vie dans leur signature, qui sont toutes "capturées" dans le futur auquel la fonction est évaluée lorsqu'elle est appelée. Cependant, l'abaissement actuel à impl Future à l'intérieur du compilateur ne prend pas en charge plusieurs durées de vie d'entrée ; un refactor plus profond est nécessaire pour faire
ce travail. Étant donné que les utilisateurs sont très susceptibles d'écrire des fonctions avec plusieurs durées de vie d'entrée (probablement toutes élidées), il s'agit d'une correction de bogue bloquant.

Autres problèmes de blocage :

Étiqueter

Travail futur

Toutes ces extensions sont connues et très prioritaires du MVP sur lesquelles nous avons l'intention de travailler dès que nous aurons expédié la version initiale d'async/await.

Fermetures asynchrones

Dans la RFC initiale, nous avons également pris en charge le modificateur async en tant que modificateur sur les littéraux de fermeture, créant des fonctions asynchrones anonymes. Cependant, l'expérience de l'utilisation de cette fonctionnalité a montré qu'il reste encore un certain nombre de questions de conception à résoudre avant de se sentir à l'aise de stabiliser ce cas d'utilisation :

  1. La nature de la capture de variable devient plus compliquée dans les fermetures asynchrones et nécessite un certain support syntaxique.
  2. L'abstraction sur des fonctions asynchrones avec des durées de vie d'entrée n'est actuellement pas possible et peut nécessiter une prise en charge de langage ou de bibliothèque supplémentaire.

Prise en charge sans STD

L'implémentation actuelle de l'opérateur d'attente nécessite que TLS passe le réveil vers le bas pendant qu'il interroge le futur intérieur. Il s'agit essentiellement d'un "piratage" pour faire fonctionner la syntaxe sur les systèmes avec TLS dès que possible. À long terme, nous n'avons pas l'intention de nous engager dans cette utilisation de TLS et préférerions passer le waker comme argument de fonction normal. Cependant, cela nécessite des modifications plus profondes du code de génération de la machine d'état afin qu'il puisse gérer la prise d'arguments.

Bien que nous ne bloquions pas la mise en œuvre de ce changement, nous le considérons comme une priorité élevée car il empêche l'utilisation d'async/wait sur les systèmes sans prise en charge TLS. Il s'agit d'un pur problème d'implémentation : rien dans la conception du système ne nécessite l' utilisation de TLS.

Méthodes de traits asynchrones

Nous n'autorisons actuellement pas les fonctions ou méthodes associées asynchrones dans les traits ; c'est le seul endroit où vous pouvez écrire fn mais pas async fn . Les méthodes asynchrones seraient très clairement une abstraction puissante et nous voulons les prendre en charge.

Une méthode async serait fonctionnellement traitée comme une méthode renvoyant un type associé qui implémenterait le futur ; chaque méthode async générerait un futur type unique pour la machine à états en laquelle cette méthode se traduit.

Cependant, étant donné que ce futur capturerait toutes les entrées, toute durée de vie ou paramètre de type d'entrée devrait également être capturé dans cet état. Cela équivaut à un concept appelé types associés génériques , une fonctionnalité que nous souhaitions depuis longtemps mais que nous n'avons pas encore correctement implémentée. Ainsi, la résolution des méthodes asynchrones est liée à la résolution des types génériques associés.

Il y a aussi des problèmes de conception en suspens. Par exemple, les méthodes asynchrones sont-elles interchangeables avec les méthodes renvoyant de futurs types qui auraient la même signature ? De plus, les méthodes asynchrones présentent des problèmes supplémentaires concernant les traits automatiques, car vous devrez peut-être exiger que le futur renvoyé par une méthode asynchrone implémente un trait automatique lorsque vous faites abstraction d'un trait avec une méthode asynchrone.

Une fois que nous avons même cette prise en charge minimale, il y a d'autres considérations de conception pour les extensions futures, comme la possibilité de rendre les méthodes asynchrones « objets sécurisés ».

Générateurs et générateurs asynchrones

Nous avons une fonctionnalité de générateur instable utilisant la même transformation de machine d'état de coroutine pour prendre des fonctions qui produisent plusieurs valeurs et les transformer en machines d'état. Le cas d'utilisation le plus évident de cette fonctionnalité est de créer des fonctions qui se compilent en "itérateurs", tout comme les fonctions asynchrones se compilent pour
futurs. De même, nous pourrions composer ces deux fonctionnalités pour créer des générateurs asynchrones - des fonctions qui se compilent en "flux", l'équivalent asynchrone des itérateurs. Il existe des cas d'utilisation très clairs pour cela dans la programmation réseau, qui implique souvent l'envoi de flux de messages entre les systèmes.

Les générateurs ont beaucoup de questions de conception ouvertes car ils sont une fonctionnalité très flexible avec de nombreuses options possibles. La conception finale des générateurs dans Rust en termes de syntaxe et d'API de bibliothèque est encore très incertaine et incertaine.

A-async-await AsyncAwait-Focus F-async_await I-nominated T-lang disposition-merge finished-final-comment-period

Commentaire le plus utile

La période de commentaires finale, avec une disposition à fusionner , conformément à l' examen ci - terminée .

En tant que représentant automatisé du processus de gouvernance, je tiens à remercier l'auteur pour son travail et tous ceux qui ont contribué.

Le RFC sera bientôt fusionné.

Tous les 58 commentaires

@rfcbot fusion fcp

Le membre de l'équipe @withoutboats a proposé de fusionner cela. L'étape suivante est l'examen par le reste des membres de l'équipe tagués :

  • [x] @Centril
  • [x] @cramertj
  • [x] @eddyb
  • [x] @joshtriplett
  • [x] @nikomatsakis
  • [ ] @pnkfelix
  • [x] @scottmcm
  • [x] @sansbateaux

Préoccupations :

Une fois qu'une majorité d'examinateurs approuve (et au plus 2 approbations sont en attente), cela entrera dans sa dernière période de commentaires. Si vous repérez un problème majeur qui n'a été soulevé à aucun moment de ce processus, veuillez en parler !

Consultez ce document pour plus d'informations sur les commandes que les membres de l'équipe tagués peuvent me donner.

(Il suffit d'enregistrer les bloqueurs existants dans le rapport ci-dessus pour s'assurer qu'ils ne glissent pas)

@rfcbot concerne la mise en œuvre-blocage-des-travail-stabilisation

Le membre de l'équipe ... a proposé de fusionner ce

Comment fusionner un problème Github (pas une pull request) ?

@vi Le bot est juste un peu idiot et ne vérifie pas s'il s'agit d'un problème ou de relations publiques :) Vous pouvez remplacer "fusionner" par "accepter" ici.

Wow, merci pour le résumé complet! Je n'ai suivi que de manière tangentielle, mais je suis complètement convaincu que vous maîtrisez tout.

@rfcbot examiné

Serait-il possible d'ajouter explicitement « Triage AsyncAwait-Unclear issues » aux bloqueurs de stabilisation (et/ou d'enregistrer une préoccupation à ce sujet) ?

J'ai https://github.com/rust-lang/rust/issues/60414 que je pense important (évidemment, c'est mon bug :p), et j'aimerais au moins qu'il soit explicitement différé avant la stabilisation :)

Je voudrais juste remercier la communauté pour l'effort que les équipes Rust ont mis dans cette fonctionnalité ! Il y a eu beaucoup de conception, de discussions et quelques pannes de communication, mais au moins moi, et j'espère que beaucoup d'autres, sont convaincus qu'à travers tout cela, nous avons trouvé la meilleure solution possible pour Rust. :tada:

(Cela dit, j'aimerais voir une mention des problèmes de pontage vers les API système basées sur l'achèvement et l'annulation asynchrone dans les possibilités futures. TL; DR, ils doivent toujours contourner les tampons possédés. C'est un problème de bibliothèque, mais un avec mention.)

J'aimerais également voir une mention des problèmes avec les API basées sur l'achèvement. (voir ce fil interne pour le contexte) Compte tenu de l'IOCP et de l'introduction de io_uring , qui pourrait devenir The Way for async IO sur Linux, je pense qu'il est important d'avoir une voie claire pour les gérer. Les idées de suppression asynchrone hypothétiques d'IIUC ne peuvent pas être implémentées en toute sécurité, et le passage des tampons possédés sera moins pratique et potentiellement moins performant (par exemple en raison d'une localité pire ou en raison de copies supplémentaires).

@newpavlov J'ai implémenté des choses similaires pour Fuchsia, et il est tout à fait possible de le faire sans drop async. Il existe différentes manières de procéder, telles que l'utilisation de la mise en commun des ressources où l'acquisition d'une ressource doit potentiellement attendre la fin d'un travail de nettoyage sur les anciennes ressources. L'API future actuelle peut et a été utilisée pour résoudre efficacement ces problèmes dans les systèmes de production.

Cependant, ce problème concerne la stabilisation d'async/wait, qui est orthogonale à la conception des futures API, qui s'est déjà stabilisée. N'hésitez pas à poser d'autres questions ou à ouvrir un sujet de discussion sur le future-rs repo.

@Ekleog

Serait-il possible d'ajouter explicitement « Triage AsyncAwait-Unclear issues » aux bloqueurs de stabilisation (et/ou d'enregistrer une préoccupation à ce sujet) ?

Oui, c'est quelque chose que nous faisons chaque semaine. WRT ce problème spécifique (#60414), je pense qu'il est important et j'aimerais le voir résolu, mais nous n'avons pas encore été en mesure de décider s'il doit ou non bloquer la stabilisation, d'autant plus qu'il est déjà observable dans -> impl Trait fonctions.

@cramertj Merci ! Je pense que le problème du #60414 est essentiellement "l'erreur peut survenir très rapidement maintenant", alors qu'avec -> impl Trait il semble que personne ne l'ait remarqué auparavant - alors ce n'est pas grave si cela est de toute façon reporté, quelques problèmes devra :) (FWIW il est apparu en code naturel dans une fonction où je renvoie à la fois () à un endroit et T::Assoc à un autre, ce que l'IIRC m'a empêché de compiler -- je n'ai pas vérifié le code depuis l'ouverture du #60414, alors peut-être que mes souvenirs sont faux)

@Ekleog Ouais c'est logique ! Je peux certainement voir pourquoi ce serait pénible - j'ai créé un flux zulip pour plonger davantage dans ce problème spécifique.

EDIT : tant pis, j'ai raté la cible 1.38 .

@cramertj

Il existe différentes manières de procéder, telles que l'utilisation de la mise en commun des ressources où l'acquisition d'une ressource doit potentiellement attendre la fin d'un travail de nettoyage sur les anciennes ressources.

Ne sont-ils pas moins efficaces que de conserver des tampons dans le cadre de l'état futur ? Ma principale préoccupation est que la conception actuelle ne sera pas à coût zéro (dans le sens où vous pourrez créer un code plus efficace en supprimant l'abstraction async ) et moins ergonomique sur les API basées sur la complétion, et il y a pas de moyen clair pour le réparer. Ce n'est en aucun cas un obstacle, mais je pense qu'il est important de ne pas oublier de telles lacunes dans la conception, d'où la demande de le mentionner dans l'OP.

@le duc

L'équipe lang peut bien sûr en juger mieux que moi, mais retarder à 1.38 pour assurer une implémentation stable semblerait beaucoup plus judicieux.

Ce problème cible 1.38, voir la première ligne de description.

@huxi merci, j'ai raté ça. J'ai édité mon commentaire.

@newpavlov

Ne sont-ils pas moins efficaces que de conserver des tampons dans le cadre de l'état futur ? Ma principale préoccupation est que la conception actuelle ne sera pas à coût nul (dans le sens où vous pourrez créer un code plus efficace en supprimant l'abstraction asynchrone) et moins ergonomique sur les API basées sur la complétion, et qu'il n'y a pas de moyen clair de corriger ce. Ce n'est en aucun cas un obstacle, mais je pense qu'il est important de ne pas oublier de telles lacunes dans la conception, d'où la demande de le mentionner dans l'OP.

Non, pas nécessairement, mais déplaçons cette discussion vers un problème sur un fil distinct, car il n'est pas lié à la stabilisation de async/wait.

(Cela dit, j'aimerais voir une mention des problèmes de pontage vers les API système basées sur l'achèvement et l'annulation asynchrone dans les possibilités futures. TL; DR, ils doivent toujours passer autour des tampons possédés. C'est un problème de bibliothèque, mais un avec mention.)

J'aimerais également voir une mention des problèmes avec les API basées sur l'achèvement. (voir ce fil de discussion interne pour le contexte) Compte tenu de l'IOCP et de l'introduction de io_uring, qui peut devenir la voie pour les IO asynchrones sous Linux, je pense qu'il est important d'avoir une voie claire à suivre pour les gérer.

Je suis d'accord avec Taylor sur le fait que discuter des conceptions d'API dans cet espace de problème serait hors sujet, mais je souhaite aborder un aspect spécifique de ces commentaires (et cette discussion autour de io_uring en général) qui est pertinent pour la stabilisation async/wait : le problème de Horaire.

io_uring est une interface qui arrive sur Linux cette année 2019. Le projet Rust travaille sur l'abstraction du futur depuis 2015, il y a quatre ans. Le choix fondamental de privilégier un sondage basé sur une API basée sur l'achèvement s'est produit en 2015 et 2016. À RustCamp en 2015, Carl Lerche a Dans cet article de blog en 2016, Aaron Turon a parlé des avantages de la création d'abstractions de niveau supérieur. Ces décisions ont été prises il y a longtemps et nous n'aurions pas pu arriver au point où nous en sommes aujourd'hui sans elles.

Les suggestions que nous devrions revoir notre modèle de futur sous-jacent sont des suggestions que nous devrions revenir à l'état dans lequel nous étions il y a 3 ou 4 ans, et recommencer à partir de ce point. Quel type d'abstraction pourrait couvrir un modèle d'E/S basé sur la complétion sans introduire de surcharge pour les primitives de niveau supérieur, comme l'a décrit Aaron ? Comment allons-nous mapper ce modèle à une syntaxe qui permet aux utilisateurs d'écrire « Rouille normale + annotations mineures » comme le fait async/wait ? Comment pourrons-nous gérer l'intégration de cela dans notre modèle de mémoire, comme nous l'avons fait pour ces machines à états avec broche ? Essayer de fournir des réponses à ces questions serait hors sujet pour ce fil; le fait est que leur répondre et prouver que les réponses sont correctes, c'est du travail. Ce qui équivaut à une solide décennie d'années de travail entre les différents contributeurs jusqu'à présent devrait être refait.

L'objectif de Rust est d'expédier un produit que les gens peuvent utiliser, et cela signifie que nous devons expédier . Nous ne pouvons pas toujours nous arrêter pour regarder vers l'avenir ce qui pourrait devenir un gros problème l'année prochaine et redémarrer notre processus de conception pour l'intégrer. Nous faisons de notre mieux en fonction de la situation dans laquelle nous nous trouvons. Évidemment, il peut être frustrant d'avoir l'impression que nous avons à peine raté quelque chose, mais dans l'état actuel des choses, nous n'avons pas non plus une vue complète a) du meilleur résultat pour la gestion de io_uring sera, b) quelle sera l'importance de io_uring dans l'écosystème dans son ensemble. Nous ne pouvons pas revenir sur 4 ans de travail sur cette base.

Il existe déjà des limitations similaires, probablement encore plus graves, de Rust dans d'autres espaces. Je veux en souligner un que j'ai examiné avec Nick Fitzgerald l'automne dernier : l'intégration de wasm GC. Le plan de gestion des objets gérés dans wasm consiste essentiellement à segmenter l'espace mémoire, de sorte qu'ils existent dans un espace d'adressage distinct des objets non gérés (en fait, un jour dans de nombreux espaces d'adressage distincts). Le modèle de mémoire de Rust n'est tout simplement pas conçu pour gérer des espaces d'adressage séparés, et tout code dangereux qui traite de la mémoire de tas suppose aujourd'hui qu'il n'y a qu'un seul espace d'adressage. Bien que nous ayons esquissé à la fois des solutions techniques révolutionnaires et techniquement non révolutionnaires, mais extrêmement perturbatrices, la voie à suivre la plus probable est d'accepter que notre histoire wasm GC n'est peut-être pas parfaitement optimale , car nous traitons avec les limites de Rust comme ça existe.

Un aspect intéressant que nous stabilisons ici est que nous rendons disponibles des structures auto-référentielles à partir de code sécurisé. Ce qui rend cela intéressant, c'est que dans un Pin<&mut SelfReferentialGenerator> , nous avons une référence mutable (stockée sous forme de champ dans le Pin ) pointant vers l'état entier du générateur, et nous avons un pointeur à l'intérieur de cet état pointant à un autre morceau de l'État. Ce pointeur interne aliase avec la référence mutable !

La référence mutable, à ma connaissance, ne s'habitue pas à accéder réellement à la partie de la mémoire que le pointeur vers un autre champ pointe ainsi. (En particulier, il n'y a pas clone méthode

Il y a probablement peu de choses que nous puissions faire à ce sujet à ce stade, en particulier puisque Pin est déjà stable, mais je pense qu'il vaut la peine de souligner que cela compliquera considérablement quelles que soient les règles pour lesquelles l'alias est autorisé et qui n'est pas. Si vous pensiez que Stacked Borrows était compliqué, préparez-vous à ce que les choses empirent.

Cc https://github.com/rust-lang/unsafe-code-guidelines/issues/148

La référence mutable, à ma connaissance, ne s'habitue pas à accéder réellement à la partie de la mémoire que le pointeur vers un autre champ pointe ainsi.

Les gens ont parlé de faire en sorte que tous ces types de coroutines implémentent Debug , il semble que cette conversation devrait également intégrer des directives de code non sécurisées pour être sûr de ce qu'il est sûr de déboguer.

Les gens ont parlé de faire en sorte que tous ces types de coroutines implémentent le débogage, il semble que cette conversation devrait également intégrer des directives de code non sécurisées pour être sûr de ce qu'il est sûr de déboguer.

En effet. Une telle implémentation Debug , si elle imprime les champs auto-référencés, interdirait probablement les optimisations basées sur les références au niveau MIR dans les générateurs.

Mise à jour concernant les bloqueurs :

Les deux bloqueurs de haut niveau ont tous les deux fait de gros progrès et pourraient en fait être tous les deux terminés (?). Plus d'infos de @cramertj @tmandry et @nikomatsakis à ce sujet seraient super :

  • Le problème des durées de vie multiples aurait dû être résolu par le #61775
  • La question de la taille est plus ambiguë ; il y aura toujours plus d'optimisations à faire, mais je pense que le fruit à portée de main d'éviter une augmentation exponentielle évidente des footguns a été en grande partie résolu?

Cela laisse la documentation et les tests comme les principaux obstacles à la stabilisation de cette fonctionnalité. @Centril a toujours exprimé des inquiétudes quant au fait que la fonctionnalité n'est pas suffisamment testée ou raffinée ; @Centril y a-t-il un endroit où vous avez énuméré des problèmes spécifiques qui peuvent être cochés pour conduire cette fonctionnalité à la stabilisation ?

Je ne sais pas si quelqu'un a des papiers. Quiconque souhaite se concentrer sur l'amélioration de la documentation dans l'arborescence dans le livre, la référence, etc. rendrait un grand service ! La documentation hors de l'arbre comme dans le référentiel à terme ou areweasyncyet a un peu de temps supplémentaire.

À partir d'aujourd'hui, nous avons 6 semaines jusqu'à ce que la version bêta soit coupée, alors disons que nous avons 4 semaines (jusqu'au 1er août) pour faire ces choses pour être sûr que nous ne glisserons pas 1,38.

La question de la taille est plus ambiguë ; il y aura toujours plus d'optimisations à faire, mais je pense que le fruit à portée de main d'éviter une augmentation exponentielle évidente des footguns a été en grande partie résolu?

Je crois que oui, et d'autres ont également été fermés récemment; mais il y a d'autres problèmes de blocage .

@Centril y a-t-il un endroit où vous avez énuméré des problèmes spécifiques qui peuvent être cochés pour conduire cette fonctionnalité à la stabilisation ?

Il y a un papier dropbox avec une liste de choses que nous voulions tester et il y a https://github.com/rust-lang/rust/issues/62121. En dehors de cela, je vais essayer de réexaminer les domaines qui, selon moi, sont sous-testés dès que possible. Cela dit, certains domaines sont maintenant assez bien testés.

Quiconque souhaite se concentrer sur l'amélioration de la documentation dans l'arborescence dans le livre, la référence, etc. rendrait un grand service !

En effet; Je serais heureux d'examiner les PR à la référence. Aussi cc @ehuss.


Je voudrais également déplacer async unsafe fn du MVP dans sa propre porte de fonctionnalité parce que je pense que a) il a été peu utilisé, b) il n'est pas particulièrement bien testé, c) il se comporte apparemment bizarrement parce que le .await n'est pas celui où vous écrivez unsafe { ... } et cela est compréhensible à partir du "point de vue d'implémentation qui fuit" mais pas tant à partir d'un point de vue d'effets, d) il a fait l'objet de peu de discussions et n'a pas été inclus dans la RFC ni ce rapport, et e) nous l'avons fait avec const fn et cela a bien fonctionné. (Je peux rédiger la fonction de synchronisation PR)

Je suis d' async unsafe fn déstabiliser

J'ai créé https://github.com/rust-lang/rust/issues/62500 pour déplacer async unsafe fn vers une porte de fonctionnalité distincte et je l'ai répertorié comme bloqueur. Nous devrions probablement aussi créer un problème de suivi approprié, je suppose.

Je suis fortement sceptique quant à l'obtention d'un design différent pour async unsafe fn et je suis surpris par la décision de ne pas l'inclure dans le cycle initial de stabilisation. J'ai écrit un certain nombre de async fn qui sont dangereux et qui les rendront async fn really_this_function_is_unsafe() ou quelque chose, je suppose. Cela semble être une régression dans une attente de base des utilisateurs de Rust en termes de capacité à définir des fonctions nécessitant unsafe { ... } appel de async / await est inachevé.

@cramertj semble que nous devrions discuter! J'ai créé un sujet Zulip pour cela , pour essayer d'éviter que ce problème de suivi ne soit trop surchargé.

Concernant les tailles futures, les cas qui affectent chaque point await sont optimisés. Le dernier problème que je connaisse est le #59087, où tout emprunt d'un futur avant d'attendre peut doubler la taille allouée pour ce futur. C'est assez malheureux, mais c'est quand même un peu mieux que là où nous étions avant.

J'ai une idée de la façon de résoudre ce problème, mais à moins que cela ne soit beaucoup plus courant que je ne le pense, cela ne devrait probablement pas être un bloqueur pour un MVP stable.

Cela dit, il me reste à regarder l'impact de ces optimisations sur Fuchsia (qui est bloqué depuis un moment mais devrait s'éclaircir aujourd'hui ou demain). Il est tout à fait possible que nous découvrions d'autres cas et que nous devions décider si l'un d'entre eux doit être bloquant.

@cramertj (Rappel : j'utilise async/await et je veux qu'il se stabilise dès que possible) Votre argument ressemble à un argument pour retarder la stabilisation d'async/await, pas pour stabiliser async unsafe ce moment sans expérimentation et réflexion appropriées.

D'autant plus qu'il n'était pas inclus dans la RFC et qu'il déclencherait potentiellement un autre shitstorm de « trait d'impl en position d'argument » s'il était forcé de sortir de cette façon.

[Note latérale qui ne mérite pas vraiment de discussion ici : pour « Encore une autre fonctionnalité de porte contribuera à l'impression qu'async/await n'est pas terminé », j'ai trouvé un bogue toutes les quelques heures d'utilisation d'async/await, répandu par quelques-uns mois légitimement nécessaires à l'équipe rustc pour les réparer, et c'est la chose qui me fait dire que c'est inachevé. Le dernier a été corrigé il y a quelques jours, et j'espère vraiment ne pas en découvrir un autre lorsque j'essaierai à nouveau de compiler mon code avec un rustc plus récent, mais…]

Votre argument ressemble à un argument pour retarder la stabilisation d'async/wait, pas pour stabiliser l'async dangereuse en ce moment sans expérimentation et réflexion appropriées.

Non, ce n'est pas un argument pour ça. Je crois que async unsafe est prêt, et je ne peux imaginer aucun autre design pour cela. Je crois qu'il n'y a que des conséquences négatives à ne pas l'inclure dans cette version initiale. Je ne crois pas que retarder async / await dans son ensemble, ni async unsafe particulier, produira un meilleur résultat.

Je ne peux pas imaginer un autre design pour ça

Une conception alternative, bien que nécessitant certainement des extensions compliquées : async unsafe fn est unsafe à .await , pas à call() . Le raisonnement derrière cela étant que _rien de dangereux ne peut être fait_ au point où le async fn est appelé et crée le impl Future . Tout ce que fait cette étape est de mettre des données dans une structure (en fait, tous les async fn sont const à appeler). Le point réel d'insécurité est d'avancer vers l'avenir avec poll .

(à mon humble avis, si le unsafe est immédiat, unsafe async fn plus de sens, et si le unsafe est retardé, async unsafe fn plus de sens.)

Bien sûr, si nous obtenons jamais une façon de dire par exemple unsafe Future où toutes les méthodes de Future ne sont pas sûrs d'appeler, puis « levage » le unsafe à la création de impl Future , et le contrat de ce unsafe étant d'utiliser le futur résultant de manière sûre. Mais cela peut aussi être fait presque trivialement sans unsafe async fn en "désucrant" simplement manuellement un bloc async : unsafe fn os_stuff() -> impl Future { async { .. } } .

En plus de cela, cependant, il y a une question de savoir s'il existe réellement un moyen d'avoir des invariants qui doivent être conservés une fois que poll ing démarre et qui n'ont pas besoin d'être conservés lors de la création. C'est un modèle courant dans Rust que vous utilisez un constructeur unsafe pour un type sûr (par exemple Vec::from_raw_parts ). Mais l'essentiel est qu'après la construction, le type _ne puisse pas_ être utilisé à mauvais escient ; la portée de unsafe est terminée. Cette portée de l'insécurité est la clé des garanties de Rust. Si vous introduisez un unsafe async fn qui crée un coffre-fort impl Future avec des exigences pour comment/quand il est interrogé, puis transmettez-le au code sûr, ce code sûr est soudainement à l'intérieur de votre barrière de sécurité. Et cela est _très_ susceptible de se produire dès que vous utilisez ce futur d'une autre manière que de l'attendre immédiatement, car il passera probablement par _quelque_ combinateur externe.

Je suppose que le TL;DR de ceci est qu'il y a certainement des coins de async unsafe fn qui devraient être discutés correctement avant de le stabiliser, en particulier avec la direction de const Trait potentiellement introduit (j'ai un brouillon de blog post sur la généralisation de cela à un "système d'"effets" faible" avec n'importe quel mot-clé fn -modifying). Cependant, unsafe async fn pourrait en fait être assez clair sur l'"ordre"/"positionnement" des unsafe pour se stabiliser.

Je crois qu'un trait unsafe Future basé sur les effets n'est pas seulement hors de portée de tout ce que nous savons exprimer dans le langage ou le compilateur aujourd'hui, mais que ce serait finalement une conception pire en raison de l'effet supplémentaire- polymorphisme qu'il faudrait que les combinateurs aient.

rien de dangereux ne peut être fait au moment où le fn async est appelé et crée l'impl Future. Tout ce que fait cette étape est de bourrer les données dans une structure (en fait, tous les fn async sont const à appeler). Le point réel de l'insécurité fait avancer l'avenir avec le sondage.

Il est vrai que puisqu'un async fn ne peut exécuter aucun code utilisateur avant d'être .await modifié, tout comportement indéfini serait probablement retardé jusqu'à ce que .await soit appelé. Je pense, cependant, qu'il y a une distinction importante entre le point de UB et le point de unsafe ty. Le point réel de unsafe ty est l'endroit où un auteur d'API décide qu'un utilisateur doit promettre qu'un ensemble d'invariants non vérifiables statiquement est respecté, même si le résultat de la violation de ces invariants ne causerait pas UB jusqu'à plus tard dans un autre code sûr. Un exemple courant est une fonction unsafe pour créer une valeur qui implémente un trait avec des méthodes sûres (exactement ce que c'est). J'ai vu cela utilisé pour garantir que, par exemple, les types Visitor -implémentant des traits dont les implémentations reposent sur des invariants unsafe peuvent être utilisés correctement, en exigeant unsafe pour construire le type. D'autres exemples incluent des choses comme slice::from_raw_parts , qui lui-même ne provoquera pas d'UB (invariants de validité de type mis à part), mais les accès à la tranche résultante le feront.

Je ne crois pas que async unsafe fn représente un cas unique ou intéressant ici-- il suit un modèle bien établi pour effectuer des comportements unsafe derrière une interface sûre en exigeant un unsafe constructeur.

@cramertj Le fait que vous ayez même à argumenter pour cela (et je ne suggère pas que je pense que la solution actuelle est mauvaise, ou que j'ai une meilleure idée) signifie, pour moi, que ce débat devrait être à un place que les gens qui se soucient de la rouille devraient suivre : le référentiel RFC.

Pour rappel, une citation de son readme :

Vous devez suivre ce processus si [...] :

  • Tout changement sémantique ou syntaxique de la langue qui n'est pas une correction de bogue.
  • [... et aussi des trucs non cités]

Je ne dis pas qu'il y aura un changement dans la conception actuelle. En fait, y penser quelques minutes me fait penser que c'est probablement le meilleur design auquel je puisse penser. Mais le processus est ce qui nous permet d'éviter que nos croyances ne deviennent un danger pour Rust, et nous manquons de la sagesse de nombreuses personnes qui suivent le référentiel RFC mais ne lisent pas chaque problème en ne suivant pas le processus ici.

Parfois, ne pas suivre le processus peut avoir du sens. Ici, je ne vois aucune urgence qui justifierait d'ignorer le processus juste pour éviter environ 2 semaines de retard FCP.

Alors s'il vous plaît, laissez Rust être honnête avec sa communauté sur les promesses qu'il donne dans son propre fichier readme, et gardez simplement cette fonctionnalité sous une porte de fonctionnalité jusqu'à ce qu'il y ait au moins une RFC acceptée et, espérons-le, une utilisation supplémentaire dans la nature. Qu'il s'agisse de l'ensemble de la porte de fonctionnalité async/async ou simplement d'une porte de fonctionnalité asynchrone non sécurisée, cela m'est égal, mais ne stabilisez pas quelque chose qui (AFAIK) a été peu utilisé au-delà de l'async-wg et est à peine connu dans le communauté globale.

J'écris un premier passage au matériel de référence pour le livre. En cours de route, j'ai remarqué que la RFC async-wait indique que le comportement de l'opérateur ? n'a pas encore été déterminé. Et pourtant, cela semble bien fonctionner dans un bloc asynchrone ( terrain de jeu ). Devrions-nous déplacer cela vers une porte de fonctionnalité distincte ? Ou cela a-t-il été résolu à un moment donné ? Je ne l'ai pas vu dans le rapport de stabilisation, mais je l'ai peut-être manqué.

(J'ai également posé cette question sur Zulip et je préférerais des réponses là-bas, car c'est plus facile à gérer pour moi.)

Oui, cela a été discuté et résolu avec le comportement de return , break , continue et. Al. qui font tous "la seule chose possible" et se comportent comme ils le feraient à l'intérieur d'une fermeture.

let f = unsafe { || {...} }; est également sûr à appeler et IIRC équivaut à déplacer le unsafe à l'intérieur de la fermeture.
Même chose pour unsafe fn foo() -> impl Fn() { || {...} } .

Ceci, pour moi, est un précédent suffisant pour "la chose dangereuse se produit après avoir quitté la portée unsafe ".

Il en est de même pour les autres lieux. Comme indiqué précédemment, unsafe n'est pas toujours là où se trouverait l'UB potentiel. Exemple:

    let mut vec: Vec<u32> = Vec::new();

    unsafe { vec.set_len(100); }      // <- unsafe

    let val = vec.get(5).unwrap();     // <- UB
    println!("{}", val);

Cela me semble juste être une incompréhension d'unsafe - unsafe ne signifie pas qu'"une opération dangereuse se produit à l'intérieur ici" - cela indique "Je garantis que je respecte les invariants nécessaires ici". Bien que vous puissiez maintenir les invariants au point d'attente, car cela n'implique aucun paramètre variable, ce n'est pas un site très évident pour vérifier que vous respectez les invariants. Cela a beaucoup plus de sens et est beaucoup plus cohérent avec le fonctionnement de toutes nos abstractions dangereuses, pour vous garantir le respect des invariants sur le site d'appel.

Ceci est lié à la raison pour laquelle penser à un effet dangereux conduit à des intuitions inexactes (comme Ralf l'a soutenu lorsque cette idée a été évoquée pour la première fois l'année dernière). L'insécurité est spécifiquement, intentionnellement, non contagieuse. Bien que vous puissiez écrire des fonctions non sécurisées qui appellent d'autres fonctions non sécurisées et simplement transférer leurs invariants vers le haut de la pile d'appels, ce n'est pas du tout la manière normale d'utiliser unsafe, et c'est en fait un marqueur syntaxique utilisé pour définir des contrats sur des valeurs et vérifier manuellement cela vous les soutenez.

Donc ce n'est pas le cas que chaque décision de conception nécessite un RFC complet, mais nous avons travaillé pour essayer de fournir plus de clarté et de structure sur la façon dont les décisions sont prises. La liste des principaux points de décision dans l'ouverture de ce numéro en est un exemple. En utilisant les outils à notre disposition, j'aimerais essayer un point de consensus structuré autour de ce problème de fns asynchrone dangereux, il s'agit donc d'un article récapitulatif avec un sondage.

async unsafe fn

async unsafe fns sont des fonctions asynchrones qui ne peuvent être appelées qu'à l'intérieur d'un bloc non sécurisé. L'intérieur de leur corps est traité comme un champ d'application dangereux. La conception alternative principale serait de rendre async unsafe fns dangereux d' attendre , plutôt que d'appeler. Il y a un certain nombre de bonnes raisons de préférer la conception dans laquelle il est dangereux d'appeler :

  1. Il est cohérent syntaxiquement avec le comportement des fns non asynchrones non sécurisées, qui sont également risquées à appeler.
  2. Il est plus cohérent avec la façon dont l'insécurité fonctionne en général. Une fonction non sécurisée est une abstraction qui dépend du maintien de certains invariants par son appelant. C'est-à-dire qu'il ne s'agit pas de marquer « où l'opération dangereuse se produit » mais « où l'invariant est garanti d'être respecté ». Il est beaucoup plus judicieux de vérifier que les invariants sont respectés sur le site d'appel, où les arguments sont réellement spécifiés, que sur le site d'attente, indépendamment du moment où les arguments ont été sélectionnés et vérifiés. Ceci est tout à fait normal pour les fonctions dangereuses en général, qui déterminent souvent certains états que d'autres fonctions sûres s'attendent à être correctes
  3. Cela est plus cohérent avec la notion de désucrage des signatures async fn, où vous pouvez modéliser la signature comme équivalente à la suppression du modificateur async et à l'encapsulation du type de retour dans le futur.
  4. L'alternative n'est pas viable à mettre en œuvre à court ou moyen terme (c'est-à-dire plusieurs années). Il n'y a aucun moyen de créer un futur qui ne soit pas sûr à interroger dans le langage Rust actuellement conçu. Une sorte d'effet "dangereux en tant qu'effet" serait un changement énorme qui aurait des implications de grande envergure et devrait traiter de la compatibilité descendante avec unsafe tel qu'il existe déjà aujourd'hui (comme des fonctions et des blocs dangereux normaux). L'ajout de fns non sécurisés asynchrone ne modifie pas de manière significative ce paysage, alors que les fns non sécurisés asynchrone selon l'interprétation actuelle de non sécurisé ont de réels cas d'utilisation pratiques à court et moyen terme.

@rfcbot ask lang "Acceptons-nous de stabiliser async unsafe fn en tant que fn async qui n'est pas sûr à appeler ?"

Je ne sais pas comment faire un sondage avec rfcbot mais je l'ai au moins nommé.

Le membre de l'équipe @withoutboats a demandé aux équipes : T-lang, pour un consensus sur :

« Acceptons-nous de stabiliser un fn asynchrone dangereux en tant que fn asynchrone qui n'est pas sûr à appeler ? »

  • [x] @Centril
  • [x] @cramertj
  • [x] @eddyb
  • [ ] @joshtriplett
  • [x] @nikomatsakis
  • [ ] @pnkfelix
  • [ ] @scottmcm
  • [x] @sansbateaux

@sansbateaux

J'aimerais essayer un point de consensus structuré autour de ce problème de fns asynchrone dangereux, il s'agit donc d'un article récapitulatif avec un sondage.

Merci pour le compte rendu. La discussion m'a convaincu que async unsafe fn tel qu'il fonctionne tous les soirs aujourd'hui se comporte bien. (Certains tests devraient probablement être ajoutés car il semblait clairsemé.) En outre, pourriez-vous s'il vous plaît modifier le rapport en haut avec des parties de votre rapport + une description du comportement de async unsafe fn ?

Il est plus cohérent avec la façon dont l'insécurité fonctionne en général. Une fonction non sécurisée est une abstraction qui dépend du maintien de certains invariants par son appelant. C'est-à-dire qu'il ne s'agit pas de marquer « où l'opération dangereuse se produit » mais « où l'invariant est garanti d'être respecté ». Il est beaucoup plus judicieux de vérifier que les invariants sont respectés sur le site d'appel, où les arguments sont réellement spécifiés, que sur le site d'attente, indépendamment du moment où les arguments ont été sélectionnés et vérifiés. Ceci est tout à fait normal pour les fonctions dangereuses en général, qui déterminent souvent certains états que d'autres fonctions sûres s'attendent à être correctes

En tant que personne ne faisant pas trop attention, je serais d'accord et je pense que la solution ici est une bonne documentation.

Je suis peut-être hors de propos ici, mais étant donné que

  • les futurs sont combinatoires par nature, il est fondamental qu'ils soient composables.
  • les points d'attente à l'intérieur d'une future implémentation sont généralement un détail d'implémentation invisible.
  • le futur est très éloigné du contexte d'exécution, avec l'utilisateur réel peut-être entre les deux plutôt qu'à la racine.

il me semble que les invariants dépendant d'un usage/comportement spécifique en attente se situent quelque part entre une mauvaise idée et impossible à régler en toute sécurité.

S'il y a des cas où la valeur de sortie attendue est ce qui est impliqué dans le maintien des invariants, je suppose que l'avenir pourrait simplement avoir une sortie qui est un wrapper nécessitant un accès non sécurisé, comme

struct UnsafeOutput<T>(T);
impl<T> UnsafeOutput<T> {
    unsafe fn unwrap(self) -> T { self.0 }
}

Étant donné que le unsafe ness est avant le async ness dans ce "précoce peu sûr", je serais beaucoup plus heureux avec l'ordre modificateur étant unsafe async fn que async unsafe fn , car unsafe (async fn) correspond beaucoup plus clairement à ce comportement que async (unsafe fn) .

J'accepterai volontiers l'un ou l'autre, mais je pense fortement que l'ordre d'emballage exposé ici a le unsafe à l'extérieur, et l'ordre des modificateurs peut aider à clarifier cela. ( unsafe est le modificateur de async fn , pas async le modificateur de unsafe fn .)

J'accepterai volontiers l'un ou l'autre, mais je pense fortement que l'ordre d'emballage exposé ici a le unsafe à l'extérieur, et l'ordre des modificateurs peut aider à clarifier cela. ( unsafe est le modificateur de async fn , pas async le modificateur de unsafe fn .)

J'étais avec toi jusqu'à ton dernier point entre parenthèses. La rédaction de est un unsafe fn (qui s'appelle dans un contexte asynchrone).

Je dirais que nous peignons le hangar async unsafe fn vélos

Je pense que async unsafe fn plus de sens, mais je pense aussi que nous devrions grammaticalement accepter n'importe quel ordre parmi async, unsafe et const. Mais async unsafe fn plus de sens pour moi avec l'idée que vous supprimez l'async et modifiez le type de retour pour le "desucrer".

L'alternative n'est pas viable à mettre en œuvre à court ou moyen terme (c'est-à-dire plusieurs années). Il n'y a aucun moyen de créer un futur qui ne soit pas sûr à interroger dans le langage Rust actuellement conçu.

FWIW J'ai rencontré un problème similaire à celui que j'ai mentionné dans la RFC2585 en ce qui concerne les fermetures à l'intérieur de unsafe fn et les traits de fonction. Je ne m'attendais pas à ce que unsafe async fn renvoie un Future avec une méthode sûre poll , mais plutôt un UnsafeFuture avec un unsafe méthode de sondage. (*) Nous pourrions alors faire en sorte que .await fonctionne également sur des UnsafeFuture lorsqu'il est utilisé à l'intérieur de blocs unsafe { } , mais pas autrement.

Ces deux futurs traits seraient un énorme changement par rapport à ce que nous avons aujourd'hui, et ils introduiraient probablement beaucoup de problèmes de composabilité. Ainsi, le navire pour explorer des alternatives a probablement navigué. D'autant plus que cela serait différent de la façon dont les traits Fn fonctionnent aujourd'hui (par exemple, nous n'avons pas UnsafeFn trait RFC2585 était que créer une fermeture à l'intérieur d'un unsafe fn renvoie une fermeture que impls Fn() , c'est-à-dire qu'elle est sûre à appeler, même si cette fermeture peut appeler des fonctions non sécurisées.

Le problème n'est pas de créer le futur « dangereux » ou de fermer, le problème est de les appeler sans prouver que cela est sûr, en particulier lorsque leurs types ne disent pas que cela doit être fait.

(*) Nous pouvons fournir un impl de couverture de UnsafeFuture pour tous les Future s, et nous pouvons également fournir UnsafeFuture une méthode unsafe pour se "déballer" en tant que Future qui est sans danger pour poll .

Voici mes deux centimes :

  • L'explication de @cramertj (https://github.com/rust-lang/rust/issues/62149#issuecomment-510166207) me convainc que les fonctions asynchrones unsafe sont la bonne conception.
  • Je préfère de loin un ordre fixe des mots-clés unsafe et async
  • Je préfère légèrement la commande unsafe async fn car la commande semble plus logique. Similaire à « une voiture électrique rapide » par rapport à « une voiture électrique rapide ». Principalement parce qu'un async fn dessucre à un fn . Il est donc logique que les deux mots-clés soient côte à côte.

Je pense que let f = unsafe { || { ... } } devrait rendre f sûr, un trait UnsafeFn ne devrait jamais être introduit, et a priori .await ing et async unsafe fn devraient être en sécurité. Tout UnsafeFuture nécessite une justification solide !

Tout cela s'ensuit parce que unsafe devrait être explicite et Rust devrait vous ramener dans un pays sûr. De plus, de ce fait, le f de ... ne devrait _pas_ être un bloc dangereux, https://github.com/rust-lang/rfcs/pull/2585 devrait être adopté, et un async unsafe fn devrait avoir un corps sûr.

Je pense que ce dernier point pourrait s'avérer assez crucial. Il est possible que chaque async unsafe fn utilise un bloc unsafe , mais de la même manière, la plupart bénéficieraient d'une analyse de sécurité, et beaucoup semblent assez complexes pour que les erreurs soient faciles.

Nous ne devons jamais contourner le vérificateur d'emprunt lors de la capture pour les fermetures en particulier.

Donc mon commentaire ici : https://github.com/rust-lang/rust/issues/62149#issuecomment -511116357 est une très mauvaise idée.

Un trait UnsafeFuture exigerait que l'appelant écrive unsafe { } pour sonder un futur, mais l'appelant n'a aucune idée des obligations qui doivent y être prouvées, par exemple, si vous obtenez un Box<dyn UnsafeFuture> est unsafe { future.poll() } sûr ? Pour tous les futurs ? Vous ne pouvez pas savoir. Donc, cela serait complètement inutile comme @rpjohnst l'a souligné sur UnsafeFn .

Exiger que Future soit toujours sûr pour les sondages est logique, et le processus de construction d'un futur qui doit être sûr pour les sondages peut être dangereux ; Je suppose que c'est ce qu'est async unsafe fn . Mais dans ce cas, l'élément fn peut documenter ce qui doit être confirmé afin que le futur renvoyé puisse être interrogé en toute sécurité.

@rfcbot mise en œuvre-travail-blocage-stabilisation

Il y a encore 2 bloqueurs d'implémentation connus à ma connaissance (https://github.com/rust-lang/rust/issues/61949, https://github.com/rust-lang/rust/issues/62517) et ce serait encore être bon d'ajouter quelques tests. Je résous mon souci de faire en sorte que rfcbot ne soit pas notre bloqueur dans le temps, puis nous bloquerons les correctifs à la place.

@rfcbot résout la mise en œuvre-blocage-du-travail-stabilisation

:bell: Ceci entre maintenant dans sa dernière période de commentaires , selon l' examen ci-dessus . :cloche:

PR de stabilisation déposé dans https://github.com/rust-lang/rust/pull/63209.

La période de commentaires finale, avec une disposition à fusionner , conformément à l' examen ci - terminée .

En tant que représentant automatisé du processus de gouvernance, je tiens à remercier l'auteur pour son travail et tous ceux qui ont contribué.

Le RFC sera bientôt fusionné.

Un aspect intéressant que nous stabilisons ici est que nous rendons disponibles des structures auto-référentielles à partir de code sécurisé. Ce qui rend cela intéressant, c'est que dans un Pin<&mut SelfReferentialGenerator>, nous avons une référence mutable (stockée sous forme de champ dans le Pin) pointant vers l'état entier du générateur, et nous avons un pointeur à l'intérieur de cet état pointant vers un autre morceau de l'état . Ce pointeur interne aliase avec la référence mutable !

À la suite de cela, @comex a réussi à écrire du code Rust asynchrone (sûr) qui viole les annotations noalias LLVM de la manière dont nous les émettons actuellement. Cependant, il semble qu'en raison de l'utilisation de TLS, il n'y ait actuellement aucune erreur de compilation.

Cette page vous a été utile?
0 / 5 - 0 notes