Rust: [Stabilisierung] async/wartet MVP

Erstellt am 26. Juni 2019  ·  58Kommentare  ·  Quelle: rust-lang/rust

Stabilisierungsziel: 1,38,0 (Beta Cut 15.08.2019)

Zusammenfassung

Dies ist ein Vorschlag zur Stabilisierung einer minimal praktikablen Async/Await-Funktion, die Folgendes umfasst:

  • async Anmerkungen zu Funktionen und Blöcken, was dazu führt, dass sie bei der Auswertung verzögert und stattdessen in die Zukunft ausgewertet werden.
  • Ein await Operator, der nur innerhalb eines async Kontexts gültig ist, der eine Zukunft als Argument verwendet und bewirkt, dass die äußere Zukunft, in der sie sich befindet, die Kontrolle übergibt, bis die erwartete Zukunft abgeschlossen ist.

Verwandte vorherige Diskussionen

RFCs:

Tracking-Probleme:

Stabilisierungen:

Wichtige Entscheidungen getroffen

  • Die Zukunft, zu der ein asynchroner Ausdruck ausgewertet wird, wird aus seinem Anfangszustand erstellt, wobei kein Textkörper ausgeführt wird, bevor er zurückgegeben wird.
  • Die Syntax für asynchrone Funktionen verwendet den "inneren" Rückgabetyp (der Typ, der dem internen return Ausdruck entspricht) anstelle des "äußeren" Rückgabetyps (der zukünftige Typ, zu dem ein Aufruf der Funktion ausgewertet wird).
  • Die Syntax für den Wait-Operator ist die "Postfix-Punkt-Syntax", expression.await , im Gegensatz zur üblicheren await expression oder einer anderen alternativen Syntax.

Umsetzungsarbeit, die Stabilisierung blockiert

  • [x] async fns sollte mehrere Lebensdauern akzeptieren können #56238
  • [x] Generatorgröße sollte nicht exponentiell wachsen #52924
  • [ ] Minimal brauchbare Dokumentation für die Async/Await-Funktion
  • [ ] Ausreichende Compilertests des Verhaltens

Zukünftige Arbeit

  • Async/await in No-std-Kontexten: async und await verlassen sich derzeit auf TLS, um zu funktionieren. Dies ist ein Implementierungsproblem, das nicht Teil des Designs ist, und obwohl es die Stabilisierung nicht blockiert, soll es schließlich gelöst werden.
  • Async-Funktionen höherer Ordnung: async als Modifizierer für Closure-Literale wird hier nicht stabilisiert. Hinsichtlich der Erfassung und Abstraktion über asynchrone Schließungen mit Lebensdauern ist mehr Designarbeit erforderlich.
  • Asynchrone Merkmalsmethoden: Dies
  • Stream-Verarbeitung: Das Paar zum Future-Merkmal in der Futures-Bibliothek ist das Stream-Merkmal, ein asynchroner Iterator. Die Integration von Unterstützung für die Manipulation von Streams in std und die Sprache ist ein wünschenswertes langfristiges Feature.
  • Optimieren von Generatordarstellungen: Es kann mehr Arbeit geleistet werden, um die Darstellung von Generatoren zu optimieren, um ihre Größe perfekter zu machen. Wir haben sichergestellt, dass dies ausschließlich ein Optimierungsproblem ist und keine semantische Bedeutung hat.

Hintergrund

Der Umgang mit nicht-blockierender E/A ist sehr wichtig für die Entwicklung leistungsstarker Netzwerkdienste, ein Zielanwendungsfall für Rust, der von Produktionsbenutzern sehr interessiert ist. Aus diesem Grund ist eine Lösung, die das Schreiben von Diensten mit non-blocking IO ergonomisch und praktikabel zu machen, seit langem ein Ziel von Rust. Die Async/Await-Funktion ist der Höhepunkt dieser Bemühungen.

Vor 1.0 hatte Rust ein Greenthreading-System, in dem Rust ein alternatives Threading-Primitive auf Sprachebene bereitstellte, das auf nicht blockierenden IOs aufbaute. Dieses System verursachte jedoch mehrere Probleme: vor allem die Einführung einer Sprachlaufzeit, die sich auf die Leistung selbst von Programmen auswirkte, die es nicht verwendeten, die FFI erheblich erhöhte und mehrere große ungelöste Designprobleme mit der Implementierung von Greenthread-Stacks hatte .

Nach der Entfernung von Greenthreads begannen die Mitglieder des Rust-Projekts, an einer alternativen Lösung basierend auf der Futures-Abstraktion zu arbeiten. Manchmal auch Promises genannt, waren Futures in anderen Sprachen als bibliotheksbasierte Abstraktion für nichtblockierende IO sehr erfolgreich, und es war bekannt, dass sie sich auf lange Sicht gut auf eine async/await-Syntax abbilden ließen, was sie nur geringfügig weniger bequem machen konnte als ein völlig unsichtbares Greenthreading-System.

Der große Durchbruch bei der Entwicklung der Future-Abstraktion war die Einführung eines umfragebasierten Modells für Futures. Während andere Sprachen ein Callback-basiertes Modell verwenden, bei dem die Zukunft selbst dafür verantwortlich ist, den Callback nach Abschluss zu planen, verwendet Rust ein pollbasiertes Modell, bei dem ein Executor dafür verantwortlich ist, den Future bis zum Abschluss abzufragen, und die Zukunft lediglich den Executor darüber zu informieren, dass er bereit ist, mit der Waker-Abstraktion weitere Fortschritte zu machen. Dieses Modell funktionierte aus mehreren Gründen gut:

  • Es ermöglichte rustc, Futures zu Zustandsautomaten zu kompilieren, die den minimalsten Speicher-Overhead sowohl in Bezug auf Größe als auch auf Indirektion hatten. Dies hat erhebliche Leistungsvorteile gegenüber dem rückrufbasierten Ansatz.
  • Es ermöglicht, dass Komponenten wie der Executor und der Reactor als Bibliotheks-APIs existieren und nicht als Teil der Sprachlaufzeit. Dies vermeidet die Einführung globaler Kosten, die sich auf Benutzer auswirken, die diese Funktion nicht verwenden, und ermöglicht Benutzern, einzelne Komponenten ihres Laufzeitsystems einfach auszutauschen, anstatt dass wir eine Blackbox-Entscheidung für sie auf Sprachebene treffen müssen.
  • Es erstellt auch alle Nebenläufigkeitsprimitiven-Bibliotheken, anstatt Nebenläufigkeit durch die Semantik der asynchronen und wartenden Operatoren in die Sprache einzubacken. Dies macht die Parallelität im Quelltext klarer und sichtbarer, der ein identifizierbares Parallelitätsprimitiv verwenden muss, um Parallelität einzuführen.
  • Es ermöglicht eine Stornierung ohne Overhead, indem ausgeführte Futures gelöscht werden können, bevor sie abgeschlossen sind. Alle Futures kostenlos kündbar zu machen, hat Vorteile hinsichtlich Leistung und Code-Klarheit für Executoren und Concurrency-Primitive.

(Die letzten beiden Punkte wurden auch als Quelle der Verwirrung für Benutzer identifiziert, die aus anderen Sprachen kommen, in denen sie nicht wahr sind, und die Erwartungen an diese Sprachen mit sich bringen. Diese Eigenschaften sind jedoch beide unvermeidbare Eigenschaften des umfragebasierten Modells die andere klare Vorteile haben und unserer Meinung nach vorteilhafte Eigenschaften sind, wenn der Benutzer sie verstanden hat.)

Das auf Umfragen basierende Modell litt jedoch unter ernsthaften ergonomischen Problemen, wenn es mit Referenzen interagierte; Im Wesentlichen führten Verweise über Streckgrenzen hinweg zu unlösbaren Kompilierungsfehlern, obwohl sie sicher sein sollten. Dies führte zu komplexem, verrauschtem Code voller Bögen, Mutexes und Bewegungsschließungen, von denen keiner unbedingt notwendig war. Selbst wenn man dieses Problem beiseite legt, ohne Primitive auf Sprachebene, litten Futures darunter, dass die Benutzer gezwungen waren, stark verschachtelte Callbacks zu schreiben.

Aus diesem Grund haben wir async/await syntaktischen Zucker mit Unterstützung für die normale Verwendung von Referenzen über die Fließgrenzen hinweg verfolgt. Nach der Einführung der Pin Abstraktion, die die Unterstützung von Referenzen über Ertragspunkte hinweg sicher machte, haben wir eine native async/await-Syntax entwickelt, die Funktionen in unsere abfragebasierten Futures kompiliert, sodass Benutzer die Leistungsvorteile asynchroner E/A mit Futures beim Schreiben von Code, der dem standardmäßigen imperativen Code sehr ähnlich ist. Dieser letzte Aspekt ist Gegenstand dieses Stabilisierungsberichts.

Beschreibung der asynchronen/erwarteten Funktion

Der Modifikator async

Das Schlüsselwort async kann an zwei Stellen verwendet werden:

  • Vor einem Blockausdruck.
  • Vor einer freien Funktion oder einer zugehörigen Funktion in einem inhärenten Impl.

_(Andere Speicherorte für asynchrone Funktionen – zum Beispiel Closure-Literale und Trait-Methoden werden in Zukunft weiterentwickelt und stabilisiert.)_

Der asynchrone Modifikator passt das Element an, das es ändert, indem es "in eine Zukunft verwandelt". Im Fall eines Blocks wird der Block nicht auf sein Ergebnis, sondern auf eine Zukunft seines Ergebnisses hin bewertet. Im Fall einer Funktion geben Aufrufe dieser Funktion eine Zukunft ihres Rückgabewerts und nicht ihres Rückgabewerts zurück. Code in einem Element, das durch einen asynchronen Modifizierer geändert wurde, wird als in einem asynchronen Kontext enthalten bezeichnet.

Der async-Modifizierer führt diese Änderung durch, indem er bewirkt, dass das Element stattdessen als reiner Konstruktor einer Zukunft ausgewertet wird, wobei Argumente und Erfassungen als Felder der Zukunft verwendet werden. Jeder Wartepunkt wird als separate Variante dieser Zustandsmaschine behandelt, und die "Poll"-Methode des Future verschiebt die Zukunft basierend auf einer Transformation des vom Benutzer geschriebenen Codes durch diese Zustände, bis sie schließlich ihren endgültigen Zustand erreicht.

Der Modifikator async move

Ähnlich wie Closures können asynchrone Blöcke Variablen im umgebenden Scope in den Zustand der Zukunft aufnehmen. Wie Closures werden diese Variablen standardmäßig als Referenz erfasst. Sie können jedoch stattdessen nach Wert erfasst werden, indem der Modifikator move (genau wie Closures). async kommt vor move , wodurch diese Blöcke zu async move { } Blöcken werden.

Der await Operator

In einem asynchronen Kontext kann ein neuer Ausdruck gebildet werden, indem ein Ausdruck mit dem await Operator unter Verwendung dieser Syntax kombiniert wird:

expression.await

Der Wait-Operator kann nur in einem asynchronen Kontext verwendet werden, und der Typ des Ausdrucks, auf den er angewendet wird, muss die Future Eigenschaft implementieren. Der Ausdruck "wait" wird zum Ausgabewert der Zukunft ausgewertet, auf die er angewendet wird.

Der Wait-Operator gibt die Kontrolle über die Zukunft, die der asynchrone Kontext auswertet, bis die Zukunft, auf die er angewendet wird, abgeschlossen ist. Diese Operation der Übergabe von Steuerelementen kann nicht in der Oberflächensyntax geschrieben werden, aber wenn dies möglich wäre (in diesem Beispiel die Syntax YIELD_CONTROL! ), würde die Entzuckerung von await ungefähr so ​​aussehen:

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

Auf diese Weise können Sie warten, bis die Auswertung von Futures in einem asynchronen Kontext abgeschlossen ist, und die Übergabe der Kontrolle über Poll::Pending nach außen an den äußersten asynchronen Kontext weiterleiten, schließlich an den Executor, auf dem der Future erzeugt wurde.

Wichtige Entscheidungspunkte

Sofort nachgeben

Unsere asynchronen Funktionen und Blöcke "liefern sofort" - ihre Konstruktion ist eine reine Funktion, die sie in einen Anfangszustand versetzt, bevor Code im Körper des asynchronen Kontexts ausgeführt wird. Keiner der Body-Codes wird ausgeführt, bis Sie mit der Abfrage dieser Zukunft beginnen.

Dies unterscheidet sich von vielen anderen Sprachen, in denen Aufrufe einer asynchronen Funktion auslösen, dass die Arbeit sofort beginnt. In diesen anderen Sprachen ist async ein von Natur aus gleichzeitiges Konstrukt: Wenn Sie eine asynchrone Funktion aufrufen, wird eine andere Aufgabe ausgelöst, die gleichzeitig mit Ihrer aktuellen Aufgabe ausgeführt wird. In Rust werden Futures jedoch nicht von Natur aus gleichzeitig ausgeführt.

Wir könnten asynchrone Elemente bis zum ersten Wartepunkt ausführen lassen, wenn sie erstellt werden, anstatt sie rein zu machen. Wir haben jedoch entschieden, dass dies verwirrender ist: Ob Code während der Konstruktion des Futures oder beim Polling ausgeführt wird, hängt von der Platzierung des ersten Waits im Body ab. Es ist einfacher zu überlegen, ob der gesamte Code während des Pollings ausgeführt wird und niemals während der Konstruktion.

Referenz:

Syntax des Rückgabetyps

Die Syntax unserer asynchronen Funktionen verwendet den Rückgabetyp „inner“ und nicht den Rückgabetyp „äußer“. Das heißt, sie sagen, dass sie den Typ zurückgeben, zu dem sie schließlich ausgewertet werden, anstatt zu sagen, dass sie eine Zukunft dieses Typs zurückgeben.

Auf einer Ebene ist dies eine Entscheidung darüber, welche Art von Klarheit bevorzugt wird: Da die Signatur auch die Annotation async , wird die Tatsache, dass sie ein Future zurückgeben, in der Signatur explizit gemacht. Für Benutzer kann es jedoch hilfreich sein, zu sehen, dass die Funktion ein Future zurückgibt, ohne dass sie auch das async-Schlüsselwort beachten müssen. Aber das fühlt sich auch wie ein Boilerplate an, da die Informationen auch durch das Schlüsselwort async übermittelt werden.

Was für uns wirklich den Ausschlag gab, war die Frage der lebenslangen Elision. Der "äußere" Rückgabetyp jeder asynchronen Funktion ist impl Future<Output = T> , wobei T der innere Rückgabetyp ist. Dieser Future erfasst jedoch auch die Lebensdauern aller Eingabeargumente selbst: Dies ist das Gegenteil der Standardeinstellung für impl Trait, bei der keine Eingabelebensdauer erfasst wird, es sei denn, Sie geben diese an. Mit anderen Worten, die Verwendung des äußeren Rückgabetyps würde bedeuten, dass asynchrone Funktionen nie von der Elisierung auf Lebenszeit profitieren (es sei denn, wir haben etwas noch Ungewöhnlicheres getan, wie z.

Wir haben entschieden, dass es angesichts der ausführlichen und ehrlichen Verwirrung des Schreibens des äußeren Rückgabetyps das zusätzliche Signalisieren nicht wert war, dass dies eine Zukunft zurückgibt, um von den Benutzern zu verlangen, sie zu schreiben.

Zerstörer-Bestellung

Die Reihenfolge von Destruktoren in asynchronen Kontexten ist dieselbe wie in nicht asynchronen Kontexten. Die genauen Regeln sind hier etwas kompliziert und außerhalb des Geltungsbereichs, aber im Allgemeinen werden Werte zerstört, wenn sie außerhalb des Geltungsbereichs liegen. Dies bedeutet jedoch, dass sie nach ihrer Verwendung noch einige Zeit bestehen bleiben, bis sie gereinigt werden. Wenn diese Zeit Wait-Anweisungen enthält, müssen diese Elemente im Zustand der Zukunft aufbewahrt werden, damit ihre Destruktoren zum richtigen Zeitpunkt ausgeführt werden können.

Als Optimierung der Größe zukünftiger Zustände könnten wir stattdessen Destruktoren in einigen oder allen Kontexten so umordnen, dass sie früher sind (z. B. könnten nicht verwendete Funktionsargumente sofort gelöscht werden, anstatt im Zustand der Zukunft gespeichert zu werden). Wir haben uns jedoch entschieden, dies nicht zu tun. Die Reihenfolge der Destruktoren kann für Benutzer ein heikles und verwirrendes Thema sein und ist manchmal sehr wichtig für die Programmsemantik. Wir haben uns entschieden, auf diese Optimierung zu verzichten, um eine möglichst einfache Destruktor-Reihenfolge zu gewährleisten - dieselbe Destruktor-Reihenfolge, wenn alle async- und await-Schlüsselwörter entfernt wurden.

(Eines Tages könnten wir daran interessiert sein, Möglichkeiten zu verfolgen, Destruktoren als rein und neu geordnet zu markieren. Das ist zukünftige Designarbeit, die auch Auswirkungen hat, die nichts mit Async/Await zu tun haben.)

Referenz:

Operatorsyntax warten

Eine wesentliche Abweichung von den Async/Await-Funktionen anderer Sprachen ist die Syntax unseres Await-Operators. Dies war Gegenstand einer enormen Diskussion, mehr als jede andere Entscheidung, die wir beim Design von Rust getroffen haben.

Rust hat seit 2015 einen Postfix ? Operator zur ergonomischen Fehlerbehandlung. Rust hat seit langem vor 1.0 auch einen Postfix-Operator . für Feldzugriff und Methodenaufrufe. Da der Kernanwendungsfall für Futures darin besteht, eine Art von IO durchzuführen, wird die überwiegende Mehrheit der Futures mit einigen Result bewertet
eine Art Fehler. In der Praxis bedeutet dies, dass fast jede Wait-Operation entweder mit einem ? oder einem Methodenaufruf danach sequenziert wird. Angesichts der standardmäßigen Vorrangstellung für Präfix- und Postfix-Operatoren hätte dies dazu geführt, dass fast jeder Wait-Operator (await future)? , was wir als sehr unergonomisch betrachteten.

Wir haben uns daher für die Verwendung einer Postfix-Syntax entschieden, die sich sehr gut mit den Operatoren ? und . . Nachdem wir viele verschiedene syntaktische Optionen in Betracht gezogen hatten, entschieden wir uns, den Operator . gefolgt vom Schlüsselwort await zu verwenden.

Referenz:

Unterstützung von Single- und Multithread-Executoren

Rust wurde entwickelt, um das Schreiben von nebenläufigen und parallelen Programmen zu vereinfachen, ohne den Leuten, die Programme schreiben, die in einem einzigen Thread laufen, Kosten aufzuerlegen. Es ist wichtig, asynchrone Funktionen sowohl auf Singlethread- als auch auf Multithread-Executoren ausführen zu können. Der Hauptunterschied zwischen diesen beiden Anwendungsfällen besteht darin, dass Multithread-Executoren die Futures, die sie erzeugen können, an Send binden, Singlethread-Executoren jedoch nicht.

Ähnlich dem bestehenden Verhalten der impl Trait Syntax "lecken" asynchrone Funktionen die Auto-Merkmale der Zukunft, die sie zurückgeben. Das heißt, der Aufrufer kann nicht nur feststellen, dass der äußere Rückgabetyp ein Future ist, sondern auch anhand einer Untersuchung seines Hauptteils feststellen, ob dieser Typ Send oder Sync ist. Dies bedeutet, dass, wenn der Rückgabetyp eines asynchronen Fns auf einen Multithread-Executor geplant wird, überprüft werden kann, ob dies sicher ist oder nicht. Allerdings ist die Art nicht zu senden erforderlich ist , und so dass die Nutzer auf singlethreaded Testamentsvollstrecker Vorteil von mehr performant singlethreaded Primitiven nehmen.

Es gab einige Bedenken, dass dies nicht gut funktionieren würde, wenn asynchrone Funktionen zu Methoden erweitert wurden, aber nach einiger Diskussion wurde festgestellt, dass die Situation nicht wesentlich anders sein würde.

Referenz:

Bekannte Stabilisierungsblocker

Staatsgröße

Ausgabe: #52924

Die Art und Weise, wie die asynchrone Transformation in eine Zustandsmaschine implementiert ist, ist derzeit überhaupt nicht optimal, wodurch der Zustand viel größer als nötig wird. Da die Zustandsgröße tatsächlich superlinear anwächst, ist es möglich, Stapelüberläufe auf dem realen Stapel auszulösen, wenn die Zustandsgröße größer als die Größe eines normalen Systemthreads wird. Die Verbesserung dieses Codegens, sodass die Größe vernünftiger ist, zumindest nicht schlimm genug, um bei normaler Verwendung Stack-Überläufe zu verursachen, ist eine blockierende Fehlerbehebung.

Mehrere Lebensdauern in asynchronen Funktionen

Ausgabe: #56238

async-Funktionen sollten in ihrer Signatur mehrere Lebensdauern haben können, die alle in der Zukunft "eingefangen" werden, auf die die Funktion beim Aufrufen ausgewertet wird. Die aktuelle Herabsetzung auf impl Future innerhalb des Compilers unterstützt jedoch nicht mehrere Eingabelebensdauern; ein tieferer Refactor ist erforderlich, um zu machen
diese Arbeit. Da Benutzer sehr wahrscheinlich Funktionen mit mehreren (wahrscheinlich alle elidierten) Eingabelebensdauern schreiben, ist dies eine blockierende Fehlerbehebung.

Andere Blockierungsprobleme:

Etikett

Zukünftige Arbeit

All dies sind bekannte Erweiterungen des MVP mit sehr hoher Priorität, an denen wir die Arbeit aufnehmen werden, sobald wir die erste Version von async/await ausgeliefert haben.

Asynchrone Schließungen

Im anfänglichen RFC haben wir auch den async-Modifizierer als Modifikator für Closure-Literale unterstützt, wodurch anonyme asynchrone Funktionen erstellt wurden. Die Erfahrung mit dieser Funktion hat jedoch gezeigt, dass noch eine Reihe von Designfragen zu lösen sind, bevor wir diesen Anwendungsfall stabilisieren können:

  1. Die Art der Variablenerfassung wird bei asynchronen Closures komplizierter und erfordert eine gewisse syntaktische Unterstützung.
  2. Das Abstrahieren über asynchrone Funktionen mit Eingabelebensdauer ist derzeit nicht möglich und erfordert möglicherweise zusätzliche Sprach- oder Bibliotheksunterstützung.

Keine-STD-Unterstützung

Die aktuelle Implementierung des Operators await erfordert, dass TLS den Waker nach unten leitet, während es die innere Zukunft abfragt. Dies ist im Wesentlichen ein "Hack", um die Syntax auf Systemen mit TLS so schnell wie möglich zum Laufen zu bringen. Auf lange Sicht haben wir nicht die Absicht, uns auf diese Verwendung von TLS festzulegen und würden es vorziehen, den Waker als normales Funktionsargument zu übergeben. Dies erfordert jedoch tiefere Änderungen am Code zur Generierung der Zustandsmaschine, damit er die Annahme von Argumenten verarbeiten kann.

Obwohl wir die Implementierung dieser Änderung nicht blockieren, betrachten wir sie als hohe Priorität, da sie die Verwendung von Async/Await auf Systemen ohne TLS-Unterstützung verhindert. Dies ist ein reines Implementierungsproblem: Nichts im Design des Systems erfordert die Verwendung

Asynchrone Merkmalsmethoden

Wir erlauben derzeit keine asynchronen verknüpften Funktionen oder Methoden in Merkmalen; Dies ist die einzige Stelle, an der Sie fn schreiben können, aber nicht async fn . Asynchrone Methoden wären ganz klar eine mächtige Abstraktion und wir möchten sie unterstützen.

Eine asynchrone Methode würde funktional wie eine Methode behandelt, die einen zugeordneten Typ zurückgibt, der future implementieren würde; jede asynchrone Methode würde einen eindeutigen zukünftigen Typ für die Zustandsmaschine generieren, in die diese Methode übersetzt.

Da diese Zukunft jedoch alle Eingaben erfassen würde, müssten auch alle Eingabelebensdauer- oder Typparameter in diesem Zustand erfasst werden. Dies entspricht einem Konzept namens generische zugeordnete Typen , eine Funktion, die wir schon lange wollten, aber noch nicht richtig implementiert haben. Somit ist die Auflösung von asynchronen Methoden an die Auflösung von generischen zugeordneten Typen gebunden.

Es gibt auch noch offene Designprobleme. Sind beispielsweise asynchrone Methoden mit Methoden austauschbar, die zukünftige Typen zurückgeben, die dieselbe Signatur haben? Darüber hinaus stellen asynchrone Methoden zusätzliche Probleme im Zusammenhang mit automatischen Merkmalen dar, da Sie möglicherweise verlangen müssen, dass die von einer asynchronen Methode zurückgegebene Zukunft ein automatisches Merkmal implementiert, wenn Sie mit einer asynchronen Methode über ein Merkmal abstrahieren.

Sobald wir auch nur diese minimale Unterstützung haben, gibt es andere Designüberlegungen für zukünftige Erweiterungen, wie die Möglichkeit, asynchrone Methoden "objektsicher" zu machen.

Generatoren und asynchrone Generatoren

Wir haben eine instabile Generatorfunktion, die dieselbe Coroutine-Zustandsmaschinentransformation verwendet, um Funktionen, die mehrere Werte liefern, in Zustandsmaschinen umzuwandeln. Der offensichtlichste Anwendungsfall für diese Funktion besteht darin, Funktionen zu erstellen, die zu "Iteratoren" kompilieren, genauso wie asynchrone Funktionen zu kompilieren
Zukunft. Auf ähnliche Weise könnten wir diese beiden Funktionen zusammensetzen, um asynchrone Generatoren zu erstellen – Funktionen, die zu "Streams" kompilieren, dem asynchronen Äquivalent von Iteratoren. In der Netzwerkprogrammierung, bei der oft Nachrichtenströme zwischen Systemen gesendet werden, gibt es dafür wirklich klare Anwendungsfälle.

Generatoren haben viele offene Designfragen, da sie ein sehr flexibles Feature mit vielen möglichen Optionen sind. Das endgültige Design für Generatoren in Rust in Bezug auf Syntax und Bibliotheks-APIs ist noch sehr in der Luft und ungewiss.

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

Hilfreichster Kommentar

Die letzte Kommentierungsfrist mit der Möglichkeit zur Fusion gemäß der obigen Überprüfung ist nun abgeschlossen .

Als automatisierter Vertreter des Governance-Prozesses möchte ich dem Autor für seine Arbeit und allen anderen, die dazu beigetragen haben, danken.

Der RFC wird in Kürze zusammengeführt.

Alle 58 Kommentare

@rfcbot fcp zusammenführen

Teammitglied @ Withoutboats hat vorgeschlagen, dies zusammenzuführen. Der nächste Schritt ist die Überprüfung durch den Rest der markierten Teammitglieder:

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

Anliegen:

Sobald die Mehrheit der Gutachter zugestimmt hat (und höchstens 2 Genehmigungen ausstehen), tritt die letzte Kommentierungsphase ein. Wenn Sie ein wichtiges Problem entdecken, das zu keinem Zeitpunkt in diesem Prozess angesprochen wurde, melden Sie sich bitte!

In diesem Dokument finden Sie Informationen darüber, welche Befehle mir markierte Teammitglieder geben können.

(Registrieren Sie einfach die vorhandenen Blocker im obigen Bericht, um sicherzustellen, dass sie nicht ausrutschen)

@rfcbot betrifft die Implementierung-Arbeitsblockierung-Stabilisierung

Teammitglied ... hat vorgeschlagen , dies zu fusionieren

Wie kann man ein Github- Problem (kein Pull-Request) zusammenführen?

@vi Der Bot ist nur ein bisschen doof und prüft nicht, ob es ein Problem oder PR ist :) Du kannst hier "merge" durch "accept" ersetzen.

Wow, danke für die umfassende Zusammenfassung! Ich habe es nur tangential verfolgt, bin aber absolut zuversichtlich, dass Sie alles im Griff haben.

@rfcbot überprüft

Könnte es möglich sein, den Stabilisierungsblockern explizit „Triage AsyncAwait-Unclear Issues“ hinzuzufügen (und/oder diesbezüglich Bedenken zu melden)?

Ich habe https://github.com/rust-lang/rust/issues/60414 , das ich für wichtig halte (offensichtlich ist es mein Fehler: p) und möchte es zumindest vor der Stabilisierung explizit verschieben :)

Ich möchte der Community nur für die Mühe danken, die die Rust-Teams in diese Funktion gesteckt haben! Es gab viel Design, Diskussionen und ein paar Störungen in der Kommunikation, aber zumindest bin ich und hoffentlich viele andere zuversichtlich, dass wir durch all das die bestmögliche Lösung für Rust gefunden haben. :tada:

(Dennoch würde ich gerne die Probleme mit der Überbrückung zu vervollständigungsbasierten und asynchronen Abbruchsystem-APIs in zukünftigen Möglichkeiten erwähnen. TL; DR sie müssen immer noch eigene Puffer umgehen. Es ist ein Bibliotheksproblem, aber eins mit Erwähnung.)

Ich würde auch gerne eine Erwähnung von Problemen mit vervollständigungsbasierten APIs sehen. (Siehe diesen internen Thread für den Kontext) In Anbetracht von IOCP und der Einführung von io_uring , das der Weg für asynchrone E/A unter Linux werden könnte, denke ich, dass es wichtig ist, einen klaren Weg für den Umgang mit ihnen zu haben. Hypothetische IIUC-Ideen zum asynchronen Ablegen können nicht sicher implementiert werden, und das Übergeben von eigenen Puffern wird weniger bequem und möglicherweise weniger leistungsfähig sein (z. B. aufgrund einer schlechteren Lokalität oder aufgrund zusätzlicher Kopien).

@newpavlov Ich habe ähnliche Dinge für Fuchsia implementiert, und es ist durchaus möglich, auf async-Drop zu verzichten. Dazu gibt es verschiedene Wege, wie beispielsweise die Verwendung von Ressourcenpooling, bei dem der Erwerb einer Ressource möglicherweise warten muss, bis einige Bereinigungsarbeiten an alten Ressourcen abgeschlossen sind. Die aktuelle Futures-API kann und wurde verwendet, um diese Probleme in Produktionssystemen effektiv zu lösen.

Bei diesem Thema geht es jedoch um die Stabilisierung von Async/Await, die orthogonal zum bereits stabilisierten Futures-API-Design ist. Fühlen Sie sich frei, weitere Fragen zu stellen oder ein Thema zur Diskussion über das futures-rs Repo zu eröffnen.

@Ekleog

Könnte es möglich sein, den Stabilisierungsblockern explizit „Triage AsyncAwait-Unclear Issues“ hinzuzufügen (und/oder diesbezüglich Bedenken zu melden)?

Ja, das machen wir jede Woche. WRT dieses spezielle Problem (#60414), ich halte es für wichtig und würde es gerne behoben sehen, aber wir konnten noch nicht entscheiden, ob es die Stabilisierung blockieren sollte oder nicht, zumal es bereits in -> impl Trait -Funktionen.

@cramertj Danke! Ich denke, das Problem von #60414 ist im Grunde "der Fehler kann jetzt sehr schnell auftreten", während es bei -> impl Trait so aussieht, als hätte es vorher noch niemand bemerkt -- dann ist es in Ordnung, wenn es trotzdem verschoben wird, einige Probleme muss :) (FWIW es entstand in natürlichem Code in einer Funktion, bei der ich sowohl () an einer Stelle als auch T::Assoc an einer anderen zurückgebe, was mich IIRC nicht dazu brachte, es zu kompilieren -- habe den Code jedoch seit dem Öffnen von #60414 nicht überprüft, daher ist meine Erinnerung vielleicht falsch)

@Ekleog Ja, das macht Sinn! Ich kann definitiv verstehen, warum es ein Schmerz sein würde - ich habe einen Zulip-Stream erstellt , um mehr in dieses spezielle Problem

EDIT: Egal, ich habe das 1.38 Ziel verfehlt.

@cramertj

Dazu gibt es verschiedene Wege, wie beispielsweise die Verwendung von Ressourcenpooling, bei dem der Erwerb einer Ressource möglicherweise warten muss, bis einige Bereinigungsarbeiten an alten Ressourcen abgeschlossen sind.

Sind sie nicht weniger effizient im Vergleich zu Puffern als Teil des zukünftigen Zustands? Mein Hauptanliegen ist, dass das aktuelle Design nicht kostenlos sein wird (in dem Sinne, dass Sie einen effizienteren Code erstellen können, indem Sie die async Abstraktion fallen lassen) und weniger ergonomisch bei vervollständigungsbasierten APIs sein, und es gibt kein klarer Weg, um es zu beheben. Es ist keineswegs ein Show-Stopper, aber ich denke, es ist wichtig, solche Mängel im Design nicht zu vergessen, daher die Bitte, es in OP zu erwähnen.

@der Herzog

Das kann das lang-Team natürlich besser beurteilen als ich, aber eine Verzögerung auf 1.38 , um eine stabile Implementierung zu gewährleisten, erscheint viel sinnvoller.

Dieses Problem zielt auf 1.38 ab, siehe erste Beschreibungszeile.

@huxi danke, das habe ich übersehen. Habe meinen Kommentar bearbeitet.

@newpavlov

Sind sie nicht weniger effizient im Vergleich zu Puffern als Teil des zukünftigen Zustands? Mein Hauptanliegen ist, dass das aktuelle Design nicht kostenlos ist (in dem Sinne, dass Sie einen effizienteren Code erstellen können, indem Sie die asynchrone Abstraktion fallen lassen) und bei vervollständigungsbasierten APIs weniger ergonomisch sein wird, und es gibt keinen klaren Weg zur Behebung es. Es ist keineswegs ein Show-Stopper, aber ich denke, es ist wichtig, solche Mängel im Design nicht zu vergessen, daher die Bitte, es in OP zu erwähnen.

Nein, nicht unbedingt, aber verschieben wir diese Diskussion zu einem Thema in einem separaten Thread, da es nichts mit der Stabilisierung von Async/Await zu tun hat.

(Dennoch würde ich gerne die Probleme mit der Überbrückung zu vervollständigungsbasierten und asynchronen Abbruchsystem-APIs in zukünftigen Möglichkeiten erwähnen. TL; DR sie müssen immer noch eigene Puffer umgehen. Es ist ein Bibliotheksproblem, aber eins mit Erwähnung.)

Ich würde auch gerne eine Erwähnung von Problemen mit vervollständigungsbasierten APIs sehen. (Siehe diesen internen Thread für den Kontext) In Anbetracht von IOCP und der Einführung von io_uring, das der Weg für asynchrone IO unter Linux werden könnte, denke ich, dass es wichtig ist, einen klaren Weg für den Umgang mit ihnen zu haben.

Ich stimme Taylor zu, dass die Diskussion von API-Designs in diesem Problembereich nicht zum Thema wäre, aber ich möchte einen bestimmten Aspekt dieser Kommentare (und dieser Diskussion über io_uring im Allgemeinen) ansprechen, der für die Async/Await-Stabilisierung relevant ist: das Problem von zeitliche Koordinierung.

io_uring ist eine Schnittstelle, die dieses Jahr 2019 zu Linux kommt. Das Rust-Projekt arbeitet seit 2015, also vor vier Jahren, an der Futures-Abstraktion. Die grundlegende Entscheidung, eine Umfrage basierend auf einer vervollständigungsbasierten API zu bevorzugen, fiel in den Jahren 2015 und 2016. Auf RustCamp im Jahr 2015 sprach Carl Lerche darüber, warum er diese Wahl in mio, der zugrunde liegenden IO-Abstraktion, getroffen hat. In diesem Blogbeitrag aus dem Jahr 2016 sprach Aaron Turon über die Vorteile der Erstellung von Abstraktionen auf höherer Ebene. Diese Entscheidungen wurden vor langer Zeit getroffen und ohne sie wären wir nicht an den Punkt gekommen, an dem wir jetzt stehen.

Vorschläge, dass wir unser zugrunde liegendes Futures-Modell überdenken sollten, sind Vorschläge, dass wir zu dem Zustand zurückkehren sollten, in dem wir uns vor 3 oder 4 Jahren befanden, und von diesem Punkt aus von vorne beginnen. Welche Art von Abstraktion könnte ein auf Vervollständigung basierendes IO-Modell abdecken, ohne Overhead für Primitiven höherer Ebene einzuführen, wie es Aaron beschrieben hat? Wie werden wir dieses Modell einer Syntax zuordnen, die es Benutzern ermöglicht, "normale Rust + kleinere Anmerkungen" so zu schreiben, wie es async/await tut? Wie werden wir in der Lage sein, das in unser Speichermodell zu integrieren, wie wir es für diese Zustandsautomaten mit Pin getan haben? Der Versuch, Antworten auf diese Fragen zu geben, würde in diesem Thread nicht zum Thema gehören; Der Punkt ist, dass es Arbeit ist, sie zu beantworten und zu beweisen, dass die Antworten richtig sind. Was bisher ein solides Jahrzehnt von Arbeitsjahren zwischen den verschiedenen Beitragszahlern ausmacht, müsste noch einmal wiederholt werden.

Das Ziel von Rust ist , ein Produkt zu versenden , dass die Menschen nutzen können, und das bedeutet , wir versenden müssen. Wir können nicht immer aufhören, in die Zukunft zu schauen, was nächstes Jahr eine große Sache werden könnte, und unseren Designprozess neu zu starten, um dies zu berücksichtigen. Wir tun unser Bestes, basierend auf der Situation, in der wir uns befinden. Natürlich kann es frustrierend sein, wenn wir das Gefühl haben, nur knapp eine große Sache verpasst zu haben, aber so wie es aussieht, haben wir auch keinen vollständigen Überblick über das beste Ergebnis für den Umgang mit io_uring sein wird, b) wie wichtig io_uring im gesamten Ökosystem sein wird. Auf dieser Grundlage können wir 4 Jahre Arbeit nicht rückgängig machen.

Es gibt bereits ähnliche, wahrscheinlich noch schwerwiegendere Einschränkungen von Rust in anderen Bereichen. Ich möchte eine hervorheben, die ich mir letzten Herbst mit Nick Fitzgerald angesehen habe: wasm GC-Integration. Der Plan für den Umgang mit verwalteten Objekten in wasm besteht darin, den Speicherbereich im Wesentlichen zu segmentieren, sodass sie in einem separaten Adressraum von nicht verwalteten Objekten existieren (in der Tat eines Tages in vielen separaten Adressräumen). Das Speichermodell von Rust ist einfach nicht dafür ausgelegt, separate Adressräume zu handhaben, und jeder unsichere Code, der sich heute mit Heap-Speicher befasst, geht davon aus, dass es nur einen Adressraum gibt. Obwohl wir sowohl bahnbrechende als auch technisch nicht brechende, aber extrem störende technische Lösungen skizziert haben, besteht der wahrscheinlichste Weg darin zu akzeptieren, dass unsere wasm GC-Geschichte möglicherweise nicht perfekt ist , da wir mit den Einschränkungen von Rust als zu tun haben Es existiert.

Ein interessanter Aspekt, den wir hier stabilisieren, ist, dass wir selbstreferentielle Strukturen aus sicherem Code verfügbar machen. Das Interessante daran ist, dass wir in einem Pin<&mut SelfReferentialGenerator> eine veränderliche Referenz haben (die als Feld in der Pin gespeichert ist), die auf den gesamten Generatorzustand zeigt, und wir haben einen Zeiger innerhalb dieses Zustands, der darauf zeigt zu einem anderen Teil des Staates. Dieser innere Zeiger weist einen Alias mit der veränderlichen Referenz auf!

Die veränderliche Referenz gewöhnt sich meines Wissens nicht daran, tatsächlich auf den Teil des Speichers zuzugreifen, auf den der Zeiger auf ein anderes Feld so zeigt. (Insbesondere gibt es keine clone Methode oder so, die das Pointer-to-Feld mit einem anderen Pointer als dem selbstreferentiellen lesen würde.) Dennoch kommt dies einem veränderlichen Referenzalias mit viel näher etwas als alles andere im Kern-Ökosystem, insbesondere alles andere, was mit rustc selbst geliefert wird. Die "Linie", die wir hier fahren, wird sehr dünn, und wir müssen aufpassen, dass wir nicht all diese schönen Optimierungen verlieren, die wir basierend auf veränderlichen Referenzen vornehmen möchten.

Daran können wir derzeit wahrscheinlich wenig ändern, insbesondere da Pin bereits stabil ist, aber ich denke, es lohnt sich darauf hinzuweisen, dass dies die Regeln, für die Aliasing zulässig ist, erheblich verkomplizieren wird und was nicht ist. Wenn Sie dachten, Stacked Borrows sei kompliziert, bereiten Sie sich darauf vor, dass die Dinge noch schlimmer werden.

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

Die veränderliche Referenz gewöhnt sich meines Wissens nicht daran, tatsächlich auf den Teil des Speichers zuzugreifen, auf den der Zeiger auf ein anderes Feld so zeigt.

Die Leute haben darüber gesprochen, dass all diese Coroutinen-Typen Debug implementieren.

Die Leute haben darüber gesprochen, dass alle diese Coroutinen-Typen Debug implementieren.

In der Tat. Eine solche Debug Implementierung würde wahrscheinlich referenzbasierte Optimierungen auf MIR-Ebene innerhalb von Generatoren verbieten, wenn sie die selbstreferenzierten Felder ausgibt.

Update zu Blockern:

Die beiden High-Level-Blocker haben beide große Fortschritte gemacht und könnten tatsächlich beide fertig sein (?). Mehr Infos von @cramertj @tmandry und @nikomatsakis dazu wären

  • Das Problem mit mehreren Lebensdauern sollte durch #61775 behoben worden sein
  • Das Größenproblem ist mehrdeutig; es wird immer noch mehr Optimierungen zu tun geben, aber ich denke, die niedrig hängende Frucht der Vermeidung offensichtlicher exponentieller Zunahmen von Footguns wurde größtenteils behoben?

Damit bleiben Dokumentation und Tests die wichtigsten Blocker bei der Stabilisierung dieser Funktion. @Centril hat immer wieder Bedenken geäußert, dass die Funktion nicht gut getestet oder poliert genug ist; @Centril gibt es irgendwo spezifische Bedenken, die abgehakt werden können, um diese Funktion zur Stabilisierung zu bringen?

Ich bin mir nicht sicher, ob jemand Fahrausweise hat. Jeder, der sich auf die Verbesserung der In-Tree-Dokumentation im Buch, der Referenz usw. konzentrieren möchte, würde einen großen Dienst tun! Außerhalb der Baumdokumentation wie im Futures-Repo oder Areweasyncyet hat man etwas mehr Zeit.

Ab heute haben wir 6 Wochen Zeit, bis die Beta gekürzt wird, also sagen wir, wir haben 4 Wochen (bis zum 1. August) Zeit, um diese Dinge zu erledigen, um sicher zu sein, dass wir nicht 1.38 ausrutschen.

Das Größenproblem ist mehrdeutig; es wird immer noch mehr Optimierungen zu tun geben, aber ich denke, die niedrig hängende Frucht der Vermeidung offensichtlicher exponentieller Zunahmen von Footguns wurde größtenteils behoben?

Ich glaube schon, und einige andere wurden kürzlich auch geschlossen; aber es gibt andere Blockierungsprobleme .

@Centril gibt es irgendwo spezifische Bedenken, die abgehakt werden können, um diese Funktion zur Stabilisierung zu bringen?

Es gibt ein Dropbox-Papier mit einer Liste von Dingen, die wir testen wollten, und es gibt https://github.com/rust-lang/rust/issues/62121. Abgesehen davon werde ich versuchen, die Bereiche, die meiner Meinung nach zu wenig getestet wurden, so schnell wie möglich erneut zu überprüfen. Allerdings sind einige Bereiche mittlerweile ziemlich gut getestet.

Jeder, der sich auf die Verbesserung der In-Tree-Dokumentation im Buch, der Referenz usw. konzentrieren möchte, würde einen großen Dienst tun!

In der Tat; Gerne überprüfe ich PRs zur Referenz. Auch cc @ehuss.


Ich würde auch gerne async unsafe fn aus dem MVP in ein eigenes Feature-Gate verschieben, weil ich denke, a) es hat wenig Verwendung gefunden, b) es ist nicht besonders gut getestet, c) es verhält sich angeblich seltsam, weil das .await Punkt ist nicht, wo Sie unsafe { ... } schreiben und dies ist verständlich von "undichte Implementierung POV", aber nicht so sehr von einem Effekt-POV, d) es wurde wenig diskutiert und wurde nicht in den RFC aufgenommen noch dieser Bericht, und e) wir haben das mit const fn und es hat gut funktioniert. (Ich kann die Feature-Gating-PR aufschreiben)

Ich bin damit einverstanden, async unsafe fn zu destabilisieren, obwohl ich skeptisch bin, dass wir mit einem anderen Design als dem vorliegenden enden. Aber es scheint klug, uns Zeit zu geben, das herauszufinden!

Ich habe https://github.com/rust-lang/rust/issues/62500 erstellt, um async unsafe fn in ein eindeutiges Feature-Gate zu verschieben und es als Blocker aufgeführt. Wir sollten wahrscheinlich auch ein richtiges Tracking-Problem erstellen, denke ich.

Ich bin sehr skeptisch, dass wir für async unsafe fn ein anderes Design erreichen werden und bin überrascht von der Entscheidung, es nicht in die erste Stabilisierungsrunde aufzunehmen. Ich habe eine Reihe von async fn s geschrieben, die unsicher sind und sie zu async fn really_this_function_is_unsafe() oder so machen, nehme ich an. Dies scheint eine Regression in einer grundlegenden Erwartung zu sein, die Rust-Benutzer in Bezug auf die Fähigkeit haben, Funktionen zu definieren, deren Aufruf unsafe { ... } erfordert. Ein weiteres Feature-Gate wird zu dem Eindruck beitragen, dass async / await noch nicht

@cramertj scheint, als sollten wir diskutieren! Ich habe dafür ein Zulip-Thema erstellt , um zu verhindern, dass dieses Tracking-Problem zu überladen wird.

In Bezug auf zukünftige Größen werden die Fälle optimiert, die jeden await Punkt betreffen. Das letzte verbleibende Problem, das mir bekannt ist, ist Nr. 59087, bei dem jede Anleihe eines Futures vor dem Warten die für diesen Future zugewiesene Größe verdoppeln kann. Das ist ziemlich schade, aber immer noch um einiges besser als dort, wo wir vorher waren.

Ich habe eine Idee, wie man dieses Problem beheben kann, aber wenn dies nicht viel häufiger vorkommt, als mir bewusst ist, sollte es wahrscheinlich kein Blocker für ein stabiles MVP sein.

Trotzdem muss ich mir noch die Auswirkungen dieser Optimierungen auf Fuchsia ansehen (das war eine Weile gesperrt, sollte aber heute oder morgen geklärt werden). Es ist gut möglich, dass wir weitere Fälle entdecken und entscheiden müssen, ob einer davon blockiert werden soll.

@cramertj (Erinnerung: Ich verwende async/await und möchte, dass es so schnell wie möglich stabilisiert wird) Ihr Argument klingt wie ein Argument für die Verzögerung der Stabilisierung von async/await, nicht für die Stabilisierung von async unsafe Moment ohne richtige Experimente und Überlegungen.

Vor allem, weil es nicht in den RFC aufgenommen wurde und möglicherweise einen weiteren Shitstorm für "Impl-Trait in Argumentposition" auslösen wird, wenn es auf diese Weise herausgezwungen wird.

[Randbemerkung, die hier keine Diskussion verdient: für „Noch ein weiteres Feature-Gate wird zu dem Eindruck beitragen, dass async/await unvollendet ist“, habe ich alle paar Stunden einen Fehler gefunden, der von einigen wenigen verbreitet wird Monate, die das rustc-Team zu Recht braucht, um sie zu reparieren, und es ist die Sache, die mich sagen lässt, dass es unvollendet ist. Der letzte wurde vor ein paar Tagen behoben, und ich hoffe wirklich, dass ich keinen weiteren entdecke, wenn ich erneut versuche, meinen Code mit einem neueren rustc zu kompilieren, aber…]

Ihr Argument klingt wie ein Argument für die Verzögerung der Stabilisierung von Async/Await, nicht für die Stabilisierung von Async unsicher im Moment ohne richtige Experimente und Überlegungen.

Nein, dafür ist es kein Argument. Ich glaube, dass async unsafe fertig ist und kann mir kein anderes Design dafür vorstellen. Ich glaube, es hat nur negative Konsequenzen, wenn es nicht in diese erste Veröffentlichung aufgenommen wird. Ich glaube nicht, dass eine Verzögerung von async / await insgesamt oder async unsafe speziell zu einem besseren Ergebnis führt.

kann mir kein anderes design dafür vorstellen

Ein alternatives Design, das jedoch definitiv komplizierte Erweiterungen erfordert: async unsafe fn ist unsafe zu .await , nicht zu call() . Die Argumentation hinter diesem Wesen , dass _nothing unsicher kann an dem Punkt, wo die done_ async fn genannt wird und schafft den impl Future . Alles, was dieser Schritt tut, ist, Daten in eine Struktur zu stopfen (in der Tat sind alle async fn const zum Aufrufen). Der eigentliche Unsicherheitsfaktor besteht darin, die Zukunft mit poll voranzutreiben.

(Imho, wenn unsafe sofort eintritt, ist unsafe async fn sinnvoller, und wenn unsafe verzögert wird, ist async unsafe fn sinnvoller.)

Natürlich, wenn wir nie eine Möglichkeit finden, zB unsafe Future zu sagen, wo alle Methoden von Future nicht sicher aufgerufen werden können, dann "hochfahren" die unsafe zur Erstellung der impl Future , und der Vertrag dieser unsafe darin, den resultierenden Future auf sichere Weise zu verwenden. Dies kann aber auch fast trivial ohne unsafe async fn geschehen, indem man einfach manuell zu einem async Block "entzuckert": unsafe fn os_stuff() -> impl Future { async { .. } } .

Darüber hinaus stellt sich jedoch die Frage, ob es tatsächlich eine Möglichkeit gibt, Invarianten zu haben, die gehalten werden müssen, sobald poll ing beginnt und die bei der Erstellung nicht gehalten werden müssen. Es ist ein übliches Muster in Rust, dass Sie einen unsafe Konstruktor für einen sicheren Typ verwenden (zB Vec::from_raw_parts ). Aber entscheidend ist, dass der Typ nach der Konstruktion _nicht_ missbraucht werden kann; der Umfang von unsafe ist vorbei. Dieser Sicherheitsbereich ist der Schlüssel zu den Garantien von Rust. Wenn Sie ein unsafe async fn einführen, das ein sicheres impl Future mit Anforderungen für das Wie/wann der Abfrage erstellt, und es dann an einen sicheren Code weitergibt, befindet sich dieser sichere Code plötzlich innerhalb Ihrer Sicherheitsbarriere. Und dies wird _sehr_ wahrscheinlich passieren, sobald Sie diese Zukunft auf eine andere Weise verwenden, als sofort darauf zu warten, da sie wahrscheinlich durch _irgendeinen_ externen Kombinator geht.

Ich denke, der TL;DR davon ist, dass es definitiv Ecken von async unsafe fn , die richtig diskutiert werden sollten, bevor sie stabilisiert werden, insbesondere wenn die Richtung von const Trait möglicherweise eingeführt wird (ich habe einen Blogentwurf Post über die Verallgemeinerung auf ein "schwaches 'Effekte'-System" mit jedem fn -modifizierenden Schlüsselwort). Allerdings könnte unsafe async fn tatsächlich klar genug über die "Ordnung"/"Positionierung" der unsafe , um sich zu stabilisieren.

Ich glaube, dass ein effektbasiertes unsafe Future Merkmal nicht nur außerhalb der Reichweite von allem liegt, was wir heute in der Sprache oder dem Compiler ausdrücken können, sondern dass es aufgrund des zusätzlichen Effekts letztendlich ein schlechteres Design wäre. Polymorphismus, der von Kombinatoren benötigt wird.

An dem Punkt, an dem der async-fn aufgerufen wird, kann nichts Unsicheres getan werden und das impl Future erstellt. Alles, was dieser Schritt tut, ist, Daten in eine Struktur zu stopfen (tatsächlich sind alle asynchronen Fn-Aufrufe konstant). Der eigentliche Unsicherheitsfaktor ist, die Zukunft mit Umfragen voranzubringen.

Da ein async fn keinen Benutzercode ausführen kann, bevor .await bearbeitet wurde, würde jedes undefinierte Verhalten wahrscheinlich verzögert werden, bis .await aufgerufen wurde. Ich denke jedoch, dass es einen wichtigen Unterschied zwischen dem Punkt von UB und dem Punkt von unsafe ty gibt. Der eigentliche Punkt von unsafe ty liegt überall dort, wo ein API-Autor entscheidet, dass ein Benutzer versprechen muss, dass eine Reihe von nicht statisch überprüfbaren Invarianten erfüllt sind, selbst wenn das Ergebnis der Verletzung dieser Invarianten keine UB verursachen würde bis später in einem anderen sicheren Code. Ein gängiges Beispiel dafür ist eine unsafe Funktion, um einen Wert zu erstellen, der ein Merkmal mit sicheren Methoden implementiert (genau das ist das). Ich habe gesehen, dass dies verwendet wird, um sicherzustellen, dass zB Visitor -Trait-implementierende Typen, deren Implementierungen auf unsafe Invarianten beruhen, solide verwendet werden können, indem unsafe , um den Typ zu konstruieren. Andere Beispiele sind Dinge wie slice::from_raw_parts , die selbst keine UB verursachen (abgesehen von Typgültigkeitsinvarianten), aber Zugriffe auf das resultierende Slice tun dies.

Ich glaube nicht, dass async unsafe fn einen einzigartigen oder interessanten Fall darstellt - es folgt einem gut etablierten Muster für die Ausführung von unsafe Verhalten hinter einer sicheren Schnittstelle, indem ein unsafe erforderlich ist Konstrukteur.

@cramertj Die Tatsache, dass Sie dafür sogar argumentieren müssen (und ich

Zur Erinnerung ein Zitat aus der Readme:

Sie müssen diesen Vorgang befolgen, wenn [...] :

  • Jede semantische oder syntaktische Änderung der Sprache, die kein Bugfix ist.
  • [... und auch nicht zitiertes Zeug]

Ich sage nicht, dass sich das aktuelle Design ändern wird. Wenn ich ein paar Minuten darüber nachdenke, denke ich, dass es wahrscheinlich das beste Design ist, das mir einfällt. Aber der Prozess ermöglicht es uns zu vermeiden, dass unsere Überzeugungen zu einer Gefahr für Rust werden, und wir verpassen die Weisheit vieler Leute, die dem RFC-Repository folgen, aber nicht jede einzelne Ausgabe lesen, indem sie dem Prozess hier nicht folgen.

Manchmal kann es sinnvoll sein, dem Prozess nicht zu folgen. Hier sehe ich keine Dringlichkeit, die es rechtfertigen würde, den Prozess zu ignorieren, nur um eine zweiwöchige FCP-Verzögerung zu vermeiden.

Also lass Rust bitte ehrlich zu seiner Community sein, was die Versprechungen angeht, die es in seiner eigenen Readme gibt, und behalte dieses Feature einfach unter einem Feature-Gate, bis es zumindest einen akzeptierten RFC gibt und hoffentlich mehr davon in freier Wildbahn verwendet wird. Ob es sich um das gesamte asynchrone / wartende Feature-Gate oder nur um ein unsicheres asynchrones Feature-Gate handelt, ist mir egal, aber stabilisiere einfach nichts, das (AFAIK) über das async-wg hinaus wenig Verwendung gefunden hat und in der kaum bekannt ist Gesamtgemeinschaft.

Ich schreibe einen ersten Durchgang an Referenzmaterial für das Buch. Dabei ist mir aufgefallen, dass das async-await RFC sagt, dass das Verhalten des ? Operators noch nicht bestimmt wurde. Und doch scheint es in einem asynchronen Block ( Spielplatz ) gut zu funktionieren. Sollten wir das in ein separates Feature-Gate verschieben? Oder wurde das irgendwann gelöst? Ich habe es im Stabilisierungsbericht nicht gesehen, aber vielleicht habe ich es übersehen.

(Ich habe diese Frage auch auf Zulip gestellt und würde dort Antworten bevorzugen, da es für mich einfacher zu verwalten ist.)

Ja, es wurde zusammen mit dem Verhalten von return , break , continue usw. diskutiert und gelöst. al. die alle "das einzig Mögliche" tun und sich so verhalten, wie sie es innerhalb eines Verschlusses tun würden.

let f = unsafe { || {...} }; auch sicher aufgerufen werden und IIRC entspricht dem Verschieben von unsafe innerhalb der Closure.
Das gleiche gilt für unsafe fn foo() -> impl Fn() { || {...} } .

Dies ist für mich Präzedenzfall genug für "die unsichere Sache passiert nach dem Verlassen des unsafe Bereichs".

Das gleiche gilt für andere Orte. Wie bereits erwähnt, ist unsafe nicht immer dort, wo das potenzielle UB wäre. Beispiel:

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

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

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

Es scheint mir nur ein Missverständnis von unsicher zu sein - unsicher bedeutet nicht, dass "hier drinnen eine unsichere Operation stattfindet" - es bedeutet "Ich garantiere, dass ich hier die notwendigen Invarianten einhalte". Während Sie die Invarianten am Wartepunkt aufrechterhalten könnten, da es keine variablen Parameter gibt, ist dies kein sehr offensichtlicher Ort, um zu überprüfen, ob Sie die Invarianten aufrechterhalten. Es ist viel sinnvoller und steht im Einklang mit der Funktionsweise all unserer unsicheren Abstraktionen, um zu garantieren, dass Sie Invarianten an der Aufrufstelle beibehalten.

Dies hängt damit zusammen, warum das Denken an unsicher als Effekt zu ungenauen Intuitionen führt (wie Ralf argumentierte, als diese Idee letztes Jahr zum ersten Mal aufkam). Unsicherheit ist ausdrücklich, absichtlich, nicht ansteckend. Sie können zwar unsichere Funktionen schreiben, die andere unsichere Funktionen aufrufen und deren Invarianten einfach im Aufrufstapel nach oben weiterleiten, dies ist jedoch nicht die normale Art und Weise, wie unsicher verwendet wird, und es ist eigentlich ein syntaktischer Marker, der verwendet wird, um Verträge über Werte zu definieren und dies manuell zu überprüfen du hältst sie aufrecht.

Es ist also nicht so, dass jede Designentscheidung einen ganzen RFC erfordert, aber wir haben versucht, mehr Klarheit und Struktur bei der Entscheidungsfindung zu schaffen. Die Liste der wichtigsten Entscheidungspunkte am Anfang dieser Ausgabe ist ein Beispiel dafür. Mit den uns zur Verfügung stehenden Tools würde ich gerne einen strukturierten Konsenspunkt zu diesem Thema unsicherer asynchroner FNSs versuchen, daher ist dies ein zusammenfassender Beitrag mit einer Umfrage.

async unsafe fn

async unsafe fns sind async-Funktionen, die nur innerhalb eines unsicheren Blocks aufgerufen werden können. Ihr Körper wird als unsicheres Zielfernrohr behandelt. Das primäre alternative Design wäre, asynchrone unsichere Fns unsicher zu machen, um zu

  1. Es stimmt syntaktisch mit dem Verhalten von nicht asynchronen unsicheren Fns überein, die auch nicht sicher aufgerufen werden können.
  2. Es stimmt eher damit überein, wie unsicher im Allgemeinen funktioniert. Eine unsichere Funktion ist eine Abstraktion, die davon abhängt, dass einige Invarianten von ihrem Aufrufer aufrechterhalten werden. Das heißt, es geht nicht darum zu markieren, "wo die unsichere Operation stattfindet", sondern "wo die Invariante garantiert aufrechterhalten wird". Es ist viel sinnvoller zu überprüfen, ob die Invarianten an der Aufrufstelle, wo die Argumente tatsächlich angegeben werden, aufrechterhalten werden, als an der Wartestelle, unabhängig davon, wann die Argumente ausgewählt und überprüft wurden. Dies ist bei unsicheren Funktionen im Allgemeinen sehr normal, die oft einen Zustand bestimmen, den andere, sichere Funktionen erwarten, dass sie korrekt sind
  3. Es ist konsistenter mit dem Konzept der Entzuckerung von async-fn-Signaturen, bei denen Sie die Signatur als Äquivalent zum Entfernen des async-Modifizierers und zum Einschließen des Rückgabetyps in eine Zukunft modellieren können.
  4. Die Alternative ist kurz- oder mittelfristig (also mehrere Jahre) nicht umsetzbar. Es gibt keine Möglichkeit, eine Zukunft zu schaffen, die in der derzeit entworfenen Rust-Sprache unsicher ist. Eine Art "unsicherer Effekt" wäre eine große Änderung, die weitreichende Auswirkungen hätte und mit der Abwärtskompatibilität mit unsicheren, wie sie bereits heute existiert (wie normalen unsicheren Funktionen und Blöcken), umgehen müssen. Das Hinzufügen von asynchronen unsicheren Fns ändert diese Landschaft nicht wesentlich, während asynchrone unsichere Fns nach der aktuellen Interpretation von unsicher kurz- und mittelfristig echte praktische Anwendungsfälle haben.

@rfcbot ask lang "Akzeptieren wir die Stabilisierung von asynchronem unsicherem Fn als async-fn, dessen Aufruf unsicher ist?"

Ich habe keine Ahnung, wie man eine Umfrage mit rfcbot macht, aber ich habe sie zumindest nominiert.

Teammitglied @ Withoutboats hat Teams gefragt: T-lang, um Konsens über:

"Akzeptieren wir die Stabilisierung von asynchronem unsicherem Fn als asynchrones Fn, das nicht sicher aufgerufen werden kann?"

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

@ohneboote

Ich würde gerne einen strukturierten Konsenspunkt zu diesem Thema unsicherer asynchroner Fns ausprobieren, daher ist dies ein zusammenfassender Beitrag mit einer Umfrage.

Danke für das Aufschreiben. Die Diskussion hat mich überzeugt, dass sich async unsafe fn wie es in der Nacht heute funktioniert richtig verhält. (Einige Tests sollten wahrscheinlich hinzugefügt werden, da es spärlich aussah.) Könnten Sie den Bericht oben mit Teilen Ihres Berichts + einer Beschreibung des Verhaltens von async unsafe fn ergänzen?

Es stimmt eher damit überein, wie unsicher im Allgemeinen funktioniert. Eine unsichere Funktion ist eine Abstraktion, die davon abhängt, dass einige Invarianten von ihrem Aufrufer aufrechterhalten werden. Das heißt, es geht nicht darum zu markieren, "wo die unsichere Operation stattfindet", sondern "wo die Invariante garantiert aufrechterhalten wird". Es ist viel sinnvoller zu überprüfen, ob die Invarianten an der Aufrufstelle, wo die Argumente tatsächlich angegeben werden, aufrechterhalten werden, als an der Wartestelle, unabhängig davon, wann die Argumente ausgewählt und überprüft wurden. Dies ist bei unsicheren Funktionen im Allgemeinen sehr normal, die oft einen Zustand bestimmen, den andere, sichere Funktionen erwarten, dass sie korrekt sind

Als jemand, der nicht zu genau aufpasst, würde ich dem zustimmen und denke, dass die Lösung hier eine gute Dokumentation ist.

Ich mag hier vielleicht falsch liegen, aber in Anbetracht dessen

  • Futures sind von Natur aus kombinatorisch, es ist von grundlegender Bedeutung, dass sie zusammensetzbar sind.
  • Wartepunkte innerhalb einer zukünftigen Implementierung sind im Allgemeinen ein unsichtbares Implementierungsdetail.
  • die Zukunft ist sehr weit vom Ausführungskontext entfernt, mit dem tatsächlichen Benutzer möglicherweise dazwischen statt an der Wurzel.

es scheint mir, dass Invarianten, die von der spezifischen wartenden Nutzung/dem Verhalten abhängen, irgendwo zwischen einer schlechten Idee und unmöglich zu regieren sind.

Wenn es Fälle gibt, in denen der erwartete Ausgabewert an der Aufrechterhaltung der Invarianten beteiligt ist, gehe ich davon aus, dass die Zukunft einfach eine Ausgabe haben könnte, die ein Wrapper ist, der einen unsicheren Zugriff erfordert, wie z

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

Angesichts der Tatsache, dass die unsafe ness vor der async ness in diesem "frühen unsicheren" ist, wäre ich viel glücklicher mit der Modifikatorreihenfolge unsafe async fn als async unsafe fn , weil unsafe (async fn) dieses Verhalten viel offensichtlicher abbildet als async (unsafe fn) .

Ich akzeptiere beides gerne, aber ich bin der festen Überzeugung, dass die hier gezeigte Umbruchreihenfolge die unsafe außen trägt, und die Reihenfolge der Modifikatoren kann dazu beitragen, dies zu verdeutlichen. ( unsafe ist der Modifikator für async fn , nicht async der Modifikator für unsafe fn .)

Ich akzeptiere beides gerne, aber ich bin der festen Überzeugung, dass die hier gezeigte Umbruchreihenfolge das unsafe außen trägt, und die Reihenfolge der Modifikatoren kann dazu beitragen, dies zu verdeutlichen. ( unsafe ist der Modifikator für async fn , nicht async der Modifikator für unsafe fn .)

Ich war bei dir bis zu deinem letzten in Klammern gesetzten Punkt. @withoutboats 'writeup für mich ziemlich deutlich macht , dass, wenn die unsafety mit an der Aufrufstelle behandelt wird, was Sie tatsächlich haben , ist ein unsafe fn (die in einem Asynchron - Kontext aufgerufen werden , geschieht).

Ich würde sagen, wir malen den Fahrradschuppen async unsafe fn .

Ich denke, dass async unsafe fn sinnvoller ist, aber ich denke auch, dass wir jede Reihenfolge zwischen async, unsicher und const grammatikalisch akzeptieren sollten. Aber async unsafe fn macht für mich mehr Sinn mit der Vorstellung, dass Sie die Asynchronität entfernen und den Rückgabetyp in "desucar" ändern.

Die Alternative ist kurz- oder mittelfristig (also mehrere Jahre) nicht umsetzbar. Es gibt keine Möglichkeit, eine Zukunft zu schaffen, die in der derzeit entworfenen Rust-Sprache unsicher ist.

FWIW Ich bin auf ein ähnliches Problem gestoßen , das ich in habe, wenn es um Schließungen innerhalb von unsafe fn und den Funktionsmerkmalen geht. Ich habe nicht erwartet, dass unsafe async fn ein Future mit einer sicheren poll Methode zurückgibt, sondern stattdessen ein UnsafeFuture mit einem unsafe Poll-Methode. (*) Wir könnten dann .await auch auf UnsafeFuture s anwenden lassen, wenn es innerhalb von unsafe { } Blöcken verwendet wird, aber nicht anders.

Diese beiden zukünftigen Eigenschaften wären eine große Veränderung gegenüber dem, was wir heute haben, und sie würden wahrscheinlich viele Probleme bei der Zusammenstellbarkeit mit sich bringen. Das Schiff zur Erkundung von Alternativen ist also wohl gesegelt. Insbesondere , da dies wäre anders , wie die Fn Züge Arbeit heute (zB haben wir eine nicht UnsafeFn Charakterzug oder ähnlich, und mein Problem in RFC2585 war , dass eine Schließung innerhalb einer Schaffung von unsafe fn gibt eine Closure zurück, die impls Fn() , also sicher aufgerufen werden kann, obwohl diese Closure unsichere Funktionen aufrufen kann.

Die "unsichere" Zukunft oder die Schließung zu schaffen ist nicht das Problem, das Problem besteht darin, sie anzurufen, ohne zu beweisen, dass dies sicher ist, insbesondere wenn ihre Typen nicht sagen, dass dies getan werden muss.

(*) Wir können eine pauschale Impl von UnsafeFuture für alle Future s bereitstellen, und wir können auch UnsafeFuture eine unsafe Methode zum "Auspacken" selbst bereitstellen als Future , das für poll sicher ist.

Hier meine zwei Cent:

  • Die Erklärung von @cramertj (https://github.com/rust-lang/rust/issues/62149#issuecomment-510166207) überzeugt mich, dass unsafe asynchrone Funktionen das richtige Design sind.
  • Ich bevorzuge eine feste Reihenfolge der Schlüsselwörter unsafe und async
  • Ich bevorzuge die Reihenfolge unsafe async fn weil die Reihenfolge logischer erscheint. Ähnlich wie "ein schnelles Elektroauto" vs "ein schnelles Elektroauto". Hauptsächlich, weil ein async fn zu einem fn entzuckert. Daher ist es sinnvoll, dass die beiden Schlüsselwörter nebeneinander stehen.

Ich denke, let f = unsafe { || { ... } } sollte f sicher machen, eine Eigenschaft von UnsafeFn sollte niemals eingeführt werden, und .await sollte a priori und async unsafe fn werden sicher. Jedes UnsafeFuture braucht eine starke Begründung!

All dies folgt, weil unsafe explizit sein sollte und Rust Sie zurück in sicheres Land bringen sollte. Auch nach diesem Token sollte f ... _kein_ ein unsicherer Block sein, https://github.com/rust-lang/rfcs/pull/2585 sollte angenommen werden und ein async unsafe fn sollte einen sicheren Körper haben.

Ich denke, dieser letzte Punkt könnte sich als ziemlich entscheidend erweisen. Es ist möglich, dass jeder async unsafe fn einen unsafe Block verwendet, aber in ähnlicher Weise würden die meisten von einer Sicherheitsanalyse profitieren, und viele klingen komplex genug, um Fehler leicht zu machen.

Insbesondere bei der Erfassung von Schließungen sollten wir den Kreditprüfer niemals umgehen.

Also mein Kommentar hier: https://github.com/rust-lang/rust/issues/62149#issuecomment -511116357 ist eine sehr schlechte Idee.

Ein UnsafeFuture Merkmal würde verlangen, dass der Anrufer unsafe { } schreibt, um eine Zukunft abzufragen, aber der Anrufer hat keine Ahnung, welche Verpflichtungen dort nachgewiesen werden müssen, z. B. wenn Sie ein Box<dyn UnsafeFuture> ist unsafe { future.poll() } sicher? Für alle Zukunft? Du kannst es nicht wissen. Dies wäre also völlig nutzlos, wie @rpjohnst auf Zwietracht für ein ähnliches Merkmal von UnsafeFn hingewiesen hat.

Es ist sinnvoll, zu verlangen, dass Futures immer sicher für die Umfrage sind, und der Prozess der Konstruktion einer Zukunft, die für die Umfrage sicher sein muss, kann unsicher sein; Ich nehme an, das ist async unsafe fn . Aber in diesem Fall kann das Element fn dokumentieren, was aufrechterhalten werden muss, damit die zurückgegebene Zukunft sicher abgefragt werden kann.

@rfcbot Implementierung-Arbeitsblockierung-Stabilisierung

Es gibt meines Wissens noch 2 bekannte Implementierungsblocker (https://github.com/rust-lang/rust/issues/61949, https://github.com/rust-lang/rust/issues/62517) und es wäre noch gut sein, einige Tests hinzuzufügen. Ich löse mein Anliegen, rfcbot zeitlich nicht zu unserem Blocker zu machen, und dann werden wir stattdessen die Fixes blockieren.

@rfcbot behebt die Implementierung-Arbeitsblockierung-Stabilisierung

:bell: Dies geht jetzt in seine letzte Kommentarperiode , wie in der obigen Überprüfung beschrieben . :Klingel:

Eingereichte Stabilisierungs-PR in https://github.com/rust-lang/rust/pull/63209.

Die letzte Kommentierungsfrist mit der Möglichkeit zur Fusion gemäß der obigen Überprüfung ist nun abgeschlossen .

Als automatisierter Vertreter des Governance-Prozesses möchte ich dem Autor für seine Arbeit und allen anderen, die dazu beigetragen haben, danken.

Der RFC wird in Kürze zusammengeführt.

Ein interessanter Aspekt, den wir hier stabilisieren, ist, dass wir selbstreferentielle Strukturen aus sicherem Code verfügbar machen. Das Interessante daran ist, dass wir in einem Pin<&mut SelfReferentialGenerator> eine veränderliche Referenz (als Feld im Pin gespeichert) haben, die auf den gesamten Generatorzustand zeigt, und wir haben einen Zeiger innerhalb dieses Zustands, der auf einen anderen Teil des Zustands zeigt . Dieser innere Zeiger weist einen Alias ​​mit der veränderlichen Referenz auf!

Als Folgemaßnahme ist es @comex tatsächlich gelungen , einen (sicheren) asynchronen Rust-Code zu

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen