Design: Vorschlag: abwarten

Erstellt am 18. Mai 2020  ·  96Kommentare  ·  Quelle: WebAssembly/design

@rreverser und ich möchte einen neuen Vorschlag für WebAssembly vorschlagen: Await .

Die Motivation für den Vorschlag ist, zu WebAssembly kompilierten " synchronen " Code zu helfen, der so etwas wie das Lesen aus einer Datei tut:

fread(buffer, 1, num, file);
// the data is ready to be used right here, synchronously

Dieser Code kann nicht einfach in einer Hostumgebung implementiert werden, die hauptsächlich asynchron ist und die "Aus einer Datei lesen" asynchron implementieren würde, beispielsweise im Web.

const result = fetch("http://example.com/data.dat");
// result is a Promise; the data is not ready yet!

Mit anderen Worten, das Ziel ist es, bei dem Sync/Async -Problem zu helfen, das bei wasm im Web so häufig vorkommt.

Das Sync/Async-Problem ist ein ernstes Problem. Während neuer Code mit Blick darauf geschrieben werden kann, können große vorhandene Codebasen oft nicht umgestaltet werden, um sie zu umgehen, was bedeutet, dass sie nicht im Web ausgeführt werden können. Wir haben Asyncify , das eine Wasm-Datei instrumentiert, um das Anhalten und Fortsetzen zu ermöglichen, und das die Portierung einiger solcher Codebasen ermöglicht hat, sodass wir hier nicht vollständig blockiert sind. Die Instrumentierung des Wasm hat jedoch einen erheblichen Overhead, etwa eine 50%ige Erhöhung der Codegröße und eine durchschnittliche Verlangsamung von 50% (aber manchmal viel schlimmer), weil wir Anweisungen zum Ausschreiben / Zurücklesen im lokalen Zustand und Aufrufstapel hinzufügen und so weiter. Dieser Overhead ist eine große Einschränkung und schließt Asyncify in vielen Fällen aus!

Das Ziel dieses Vorschlags besteht darin, das Anhalten und Fortsetzen der Ausführung auf effiziente Weise zu ermöglichen (insbesondere ohne Overhead wie bei Asyncify), damit alle Anwendungen, die auf das Sync/Async-Problem stoßen, es leicht vermeiden können. Persönlich beabsichtigen wir dies hauptsächlich für das Web, wo es WebAssembly dabei helfen kann, sich besser in Web-APIs zu integrieren, aber auch Anwendungsfälle außerhalb des Webs können relevant sein.

Die Idee in Kürze

Das Kernproblem besteht hier zwischen dem synchronen Wasm-Code und der asynchronen Host-Umgebung. Unser Ansatz konzentriert sich daher auf die Grenze einer Wasm-Instanz und der Außenseite. Wenn eine neue await -Anweisung ausgeführt wird, "wartet" die wasm-Instanz konzeptionell auf etwas von außen. Was "warten" bedeutet, würde auf verschiedenen Plattformen unterschiedlich sein und ist möglicherweise nicht auf allen Plattformen relevant (wie nicht alle Plattformen den Wasm-Atomics-Vorschlag für relevant halten), aber speziell auf der Webplattform würde die Wasm-Instanz auf ein Versprechen warten und pausieren bis das löst oder ablehnt. Zum Beispiel könnte eine wasm-Instanz bei einer fetch Netzwerkoperation pausieren und etwa so in .wat geschrieben werden:

;; call an import which returns a promise
call $do_fetch
;; wait for the promise just pushed to the stack
await
;; do stuff with the result just pushed to the stack

Beachten Sie die allgemeine Ähnlichkeit mit await in JS und anderen Sprachen. Obwohl dies nicht identisch mit ihnen ist (siehe Details unten), besteht der Hauptvorteil darin, dass es das Schreiben von synchron aussehendem Code ermöglicht (oder besser gesagt, synchron aussehenden Code in wasm zu kompilieren).

Die Details

Core-Wasm-Spezifikation

Die Änderungen an der Kern-Wasm-Spezifikation sind sehr minimal:

  • Fügen Sie einen waitref -Typ hinzu.
  • Fügen Sie eine await -Anweisung hinzu.

Für jede await -Anweisung (wie call_indirect ) wird ein Typ angegeben, zum Beispiel:

;; elaborated wat from earlier, now with full types

(type $waitref_=>_i32 (func (param waitref) (result i32)))
(import "env" "do_fetch" (func $do_fetch (result waitref)))

;; call an import which returns a promise
call $do_fetch
;; wait for the promise just pushed to the stack
await (type $waitref_=>_i32)
;; do stuff with the result just pushed to the stack

Der Typ muss ein waitref erhalten und kann jeden Typ (oder nichts) zurückgeben.

await ist nur in Bezug darauf definiert, die Hostumgebung dazu zu bringen, etwas zu tun. In diesem Sinne ähnelt es der unreachable -Anweisung, die den Host im Web dazu bringt, ein RuntimeError zu werfen, aber das ist nicht in der Kernspezifikation enthalten. Ebenso sagt die Kern-Wasm-Spezifikation nur, dass await auf etwas aus der Hostumgebung warten soll, aber nicht, wie wir dies tatsächlich tun würden, was in verschiedenen Hostumgebungen sehr unterschiedlich sein kann.

Das war's für die Kern-Wasm-Spezifikation!

Wasm JS spec

Interessanter sind die Änderungen an der wasm-JS-Spezifikation (die nur JS-Umgebungen wie das Web betreffen):

  • Ein gültiger waitref -Wert ist ein JS-Versprechen.
  • Wenn ein await für ein Promise ausgeführt wird, pausiert die gesamte Wasm-Instanz und wartet darauf, dass dieses Promise aufgelöst oder abgelehnt wird.
  • Wenn das Promise aufgelöst wird, setzt die Instanz die Ausführung fort, nachdem sie den vom Promise erhaltenen Wert (falls vorhanden) auf den Stack gepusht hat.
  • Wenn das Promise ablehnt, setzen wir die Ausführung fort und lösen eine wasm-Ausnahme vom Speicherort von await aus.

Mit "die gesamte Wasm-Instanz pausiert" meinen wir, dass der gesamte lokale Zustand erhalten bleibt (der Aufrufstapel, lokale Werte usw.), sodass wir die aktuelle Ausführung später fortsetzen können, als ob wir nie angehalten hätten (natürlich kann sich der globale Zustand geändert haben, wie der Speicher beschrieben wurde). Während wir warten, funktioniert die JS-Ereignisschleife normal und andere Dinge können passieren. Wenn wir später fortfahren (wenn wir das Versprechen nicht ablehnen, in diesem Fall würde eine Ausnahme ausgelöst werden), machen wir genau dort weiter, wo wir aufgehört haben, im Grunde so, als ob wir nie angehalten hätten (aber in der Zwischenzeit sind andere Dinge passiert, und der globale Zustand kann es getan haben). geändert usw.).

Wie sieht es für JS aus, wenn es eine Wasm-Instanz aufruft, die dann pausiert? Um das zu erklären, werfen wir zunächst einen Blick auf ein allgemeines Beispiel, das bei der Portierung nativer Anwendungen auf wasm auftritt, eine Ereignisschleife:

void event_loop_iteration() {
  // ..
  while (auto task = getTask()) {
    task.run(); // this *may* be a network fetch
  }
  // ..
}

Stellen Sie sich vor, dass diese Funktion einmal pro requestAnimationFrame aufgerufen wird. Es führt die ihm übertragenen Aufgaben aus, die Folgendes umfassen können: Rendern, Physik, Audio und Netzwerkabruf. Wenn wir ein Netzwerk-Fetch-Ereignis haben, dann und nur dann führen wir am Ende eine await Anweisung auf dem Promise fetch aus. Wir können das 0 Mal für einen Anruf von event_loop_iteration oder 1 Mal oder viele Male tun. Ob wir dabei landen, wissen wir nur während der Ausführung dieses Wasms - nicht vorher und insbesondere nicht im JS-Aufrufer dieses Wasm-Exports. Dieser Aufrufer muss also bereit sein, dass die Instanz entweder pausiert oder nicht.

Eine etwas analoge Situation kann in reinem JavaScript auftreten:

function foo(bar) {
  // ..
  let result = bar(42);
  // ..
}

foo erhält eine JS-Funktion bar und ruft sie mit einigen Daten auf. In JS kann bar eine asynchrone Funktion oder eine normale sein. Wenn es asynchron ist, gibt es ein Promise zurück und beendet die Ausführung erst später. Wenn es normal ist, wird es vor der Rückkehr ausgeführt und gibt das tatsächliche Ergebnis zurück. foo kann entweder davon ausgehen, dass es weiß, um welche Art bar es sich handelt (kein Typ ist in JS geschrieben, tatsächlich ist bar möglicherweise nicht einmal eine Funktion!), oder es kann damit umgehen beide Arten von Funktionen, um ganz allgemein zu sein.

Nun, normalerweise wissen Sie genau, welche Funktionen bar sein könnten! Zum Beispiel haben Sie möglicherweise foo und die möglichen bar s in Koordination geschrieben oder genau dokumentiert, was die Erwartungen sind. Aber die Wasm/JS-Interaktion, über die wir hier sprechen, ähnelt eher dem Fall, in dem Sie keine so enge Kopplung zwischen den Dingen haben und in dem Sie tatsächlich beide Fälle handhaben müssen. Wie bereits erwähnt, erfordert das event_loop_iteration -Beispiel dies. Aber noch allgemeiner ist das Wasm oft Ihre kompilierte Anwendung, während das JS generischer "Laufzeit" -Code ist, sodass JS alle Fälle behandeln muss. JS kann dies natürlich problemlos tun, zum Beispiel mit result instanceof Promise , um das Ergebnis zu überprüfen, oder mit JS await :

async function runEventLoopIteration() {
  // await in JavaScript can handle Promises as well as regular synchronous values
  // in the same way, so the log is guaranteed to be written out consistently after
  // the operation has finished (note: this handles 0 or 1 iterations, but could be
  // generalized)
  await wasm.event_loop_iteration();
  console.log("the event loop iteration is done");
}

(Beachten Sie, dass, wenn wir das console.log nicht brauchen, wir in diesem Beispiel das JS await nicht brauchen würden und nur einen normalen Aufruf an einen Wasm-Export haben würden.)

Um das Obige zusammenzufassen, schlagen wir vor, dass das Verhalten einer pausierenden wasm-Instanz dem JS-Fall einer Funktion nachempfunden wird, die asynchron sein kann oder nicht, was wir wie folgt formulieren können:

  • Wenn ein await ausgeführt wird, verlässt die wasm-Instanz sofort wieder denjenigen, der sie aufgerufen hat (normalerweise wäre das JS, das einen wasm-Export aufruft, aber siehe Anmerkungen später). Der Aufrufer erhält ein Promise, das er verwenden kann, um zu erfahren, wann die Ausführung des Wasm abgeschlossen ist, und um ein Ergebnis zu erhalten, falls vorhanden.

Toolchain-/Bibliotheksunterstützung

Nach unserer Erfahrung mit Asyncify und verwandten Tools ist es einfach (und macht Spaß!), ein kleines JS zu schreiben, um eine wartende Wasm-Instanz zu handhaben. Abgesehen von den zuvor erwähnten Optionen könnte eine Bibliothek eine der folgenden Aktionen ausführen:

  1. Umschließen Sie eine wasm-Instanz, damit ihre Exporte immer ein Promise zurückgeben. Das gibt eine schöne einfache Schnittstelle nach außen (es fügt jedoch zusätzlichen Overhead für schnelle Aufrufe in wasm hinzu, die nicht pausieren). Das macht zum Beispiel die eigenständige Asyncify-Hilfsbibliothek .
  2. Schreiben Sie einen globalen Status, wenn eine Instanz pausiert, und überprüfen Sie dies von der JS, die die Instanz aufgerufen hat. Das macht zum Beispiel die Asyncify-Integration von Emscripten.

Auf solchen oder anderen Ansätzen kann noch viel mehr aufgebaut werden. Wir überlassen das alles lieber Toolchains und Bibliotheken, um Komplexität im Angebot und in VMs zu vermeiden.

Implementierung und Leistung

Mehrere Faktoren sollten dazu beitragen, VM-Implementierungen einfach zu halten:

  1. Eine Pause/Fortsetzen tritt nur bei einem await auf, und wir kennen ihre Positionen statisch innerhalb jeder Funktion.
  2. Wenn wir weitermachen, machen wir genau dort weiter, wo wir die Dinge verlassen haben, und wir tun dies nur einmal. Insbesondere „forken“ wir die Ausführung nie: Nichts kehrt hier zweimal zurück, im Gegensatz zu Cs setjmp oder einer Coroutine in einem System, das Klonen/Forken erlaubt.
  3. Es ist akzeptabel, wenn die Geschwindigkeit eines await langsamer ist als ein normaler Aufruf an JS, da wir auf ein Promise warten werden, was zumindest impliziert, dass ein Promise zugewiesen wurde und dass wir auf die Ereignisschleife warten ( was einen minimalen Overhead hat und möglicherweise auf andere Dinge wartet, die derzeit ausgeführt werden ). Das heißt, die Anwendungsfälle hier verlangen nicht , dass VM-Implementierer Wege finden, um await blitzschnell zu machen. Wir wollen nur, dass await im Vergleich zu den Anforderungen hier effizient ist, und erwarten insbesondere, dass es viel schneller ist als der große Overhead von Asyncify.

Angesichts des oben Gesagten besteht eine natürliche Implementierung darin, den Stapel zu kopieren, wenn wir pausieren. Dies hat zwar einen gewissen Overhead, sollte aber angesichts der Leistungserwartungen hier sehr vernünftig sein. Und wenn wir den Stapel nur kopieren, wenn wir pausieren, können wir zusätzliche Arbeit vermeiden, um uns auf das Pausieren vorzubereiten. Das heißt, es sollte keinen zusätzlichen allgemeinen Overhead geben (der sich stark von Asyncify unterscheidet!)

Beachten Sie, dass das Kopieren des Stacks hier zwar ein natürlicher Ansatz ist, aber kein völlig trivialer Vorgang, da die Kopie je nach den Interna der VM möglicherweise kein einfaches Memcpy ist. Wenn der Stack beispielsweise Zeiger auf sich selbst enthält, müssen diese entweder angepasst werden oder der Stack muss verschiebbar sein. Alternativ ist es möglich, den Stapel vor der Wiederaufnahme an seine ursprüngliche Position zurückzukopieren, da er, wie bereits erwähnt, niemals "gegabelt" wird.

Beachten Sie auch, dass nichts in diesem Vorschlag das Kopieren des Stacks erfordert . Vielleicht können einige Implementierungen dank der vereinfachenden Faktoren, die in den 3 Punkten von früher in diesem Abschnitt erwähnt wurden, andere Dinge tun. Das beobachtbare Verhalten hier ist ziemlich einfach, und eine explizite Stack-Behandlung ist nicht Teil davon.

Wir sind sehr an Feedback von VM-Implementierern zu diesem Abschnitt interessiert!

Erläuterungen

Dieser Vorschlag hält nur die WebAssembly-Ausführung an und gibt sie an den Aufrufer der wasm-Instanz zurück. Das Anhalten von Host-Stack-Frames (JS oder Browser) ist nicht zulässig. await arbeitet auf einer Wasm-Instanz und betrifft nur Stack-Frames darin.

Es ist in Ordnung, die WebAssembly-Instanz aufzurufen, während eine Pause aufgetreten ist, und mehrere Pause/Fortsetzen-Ereignisse können gleichzeitig ausgeführt werden. (Beachten Sie, dass, wenn die VM den Stack kopiert, dies nicht bedeutet, dass jedes Mal, wenn wir das Modul betreten, ein neuer Stack zugewiesen werden muss, da wir ihn nur kopieren müssen, wenn wir tatsächlich pausieren.)

Verbindung zu anderen Vorschlägen

Ausnahmen

Das Zurückweisen des Versprechens, das eine Ausnahme auslöst, bedeutet, dass dieser Vorschlag von dem Vorschlag für wasm-Ausnahmen abhängt.

Koroutinen

Der Coroutines-Vorschlag von Andreas Rossberg befasst sich auch mit dem Anhalten und Wiederaufnehmen der Ausführung. Obwohl es einige konzeptionelle Überschneidungen gibt, glauben wir nicht, dass die Vorschläge miteinander konkurrieren. Beide sind nützlich, weil sie sich auf unterschiedliche Anwendungsfälle konzentrieren. Insbesondere ermöglicht der Coroutines-Vorschlag das Umschalten von Coroutinen zwischen Inside Wasm, während der Await-Vorschlag es einer ganzen Instanz ermöglicht, auf die Außenumgebung zu warten. Und die Art und Weise, wie beides gemacht wird, führt zu unterschiedlichen Eigenschaften.

Insbesondere handhabt der Coroutinen-Vorschlag die Stack-Erstellung auf explizite Weise (Anweisungen werden bereitgestellt, um eine Coroutine zu erstellen, eine anzuhalten usw.). Der Erwartungsvorschlag spricht nur über das Anhalten und Fortsetzen und daher ist die Handhabung des Stapels implizit . Die explizite Stapelbehandlung ist geeignet, wenn Sie wissen, dass Sie bestimmte Coroutinen erstellen, während die implizite Behandlung geeignet ist, wenn Sie nur wissen, dass Sie während der Ausführung auf etwas warten müssen (siehe das vorherige Beispiel mit event_loop_iteration ).

Die Leistungsmerkmale dieser beiden Modelle können sehr unterschiedlich sein. Wenn wir zum Beispiel jedes Mal, wenn wir Code ausführen, eine Coroutine erstellen, die möglicherweise anhält (wiederum wissen wir dies oft nicht im Voraus), wodurch möglicherweise unnötig Speicher zugewiesen wird. Das beobachtete Verhalten von await ist einfacher als das, was allgemeine Coroutinen tun können, und daher möglicherweise einfacher zu implementieren.

Ein weiterer wesentlicher Unterschied besteht darin, dass await eine einzige Anweisung ist, die alles bereitstellt, was ein wasm-Modul benötigt, um die Sync/Async-Diskrepanz zu beheben, die wasm mit dem Web hat (siehe das erste .wat -Beispiel von Anfang an Anfang). Es ist auch sehr einfach auf der JS-Seite zu verwenden, die nur ein Versprechen geben und/oder empfangen kann (obwohl ein wenig Bibliothekscode zum Hinzufügen nützlich sein kann, wie bereits erwähnt, kann es sehr minimal sein).

Theoretisch könnten die beiden Vorschläge komplementär gestaltet werden. Vielleicht könnte await irgendwie eine der Anweisungen im Coroutines-Vorschlag sein? Eine andere Option besteht darin, einem await zu erlauben, auf einer Coroutine zu arbeiten (im Grunde gibt es einer wasm-Instanz eine einfache Möglichkeit, auf Coroutine-Ergebnisse zu warten).

WASI#276

Zufälligerweise wurde WASI #276 von @tqchen gepostet, gerade als wir damit fertig waren, dies zu schreiben. Wir freuen uns sehr, dies zu sehen, da es unsere Überzeugung teilt, dass Coroutinen und asynchrone Unterstützung getrennte Funktionalitäten sind.

Wir glauben, dass eine await -Anweisung helfen könnte, etwas sehr Ähnliches zu implementieren, was dort vorgeschlagen wird (Option C3), mit dem Unterschied, dass es keine speziellen asynchronen Syscalls geben müsste, sondern einige Syscalls ein waitref zurückgeben könnten. await -ed werden können.

Für JavaScript haben wir das Warten als Anhalten einer Wasm-Instanz definiert, was sinnvoll ist, da wir mehrere Instanzen sowie JavaScript auf der Seite haben können. In einigen Serverumgebungen gibt es jedoch möglicherweise nur den Host und eine einzelne wasm-Instanz, und in diesem Fall kann das Warten viel einfacher sein, vielleicht buchstäblich auf einen Dateideskriptor oder auf die GPU warten. Oder das Warten könnte die gesamte Wasm-VM anhalten, aber weiterhin eine Ereignisschleife ausführen. Wir haben hier selbst keine konkreten Vorstellungen, aber basierend auf der Diskussion in dieser Ausgabe gibt es hier vielleicht interessante Möglichkeiten, wir sind gespannt, was die Leute denken!

Sonderfall: wasm-Instanz => wasm-Instanz => warten

Wenn in einer JS-Umgebung eine wasm-Instanz pausiert, kehrt sie sofort zu demjenigen zurück, der sie aufgerufen hat. Wir haben beschrieben, was passiert, wenn der Aufrufer von JS kommt, und dasselbe passiert, wenn der Aufrufer der Browser ist (zum Beispiel, wenn wir einen setTimeout bei einem Wasm-Export gemacht haben, der pausiert; aber dort passiert nichts Interessantes, wie das zurückgegebene Promise wird einfach ignoriert). Aber es gibt einen anderen Fall, in dem der Aufruf von wasm kommt, das heißt, wo die wasm-Instanz A direkt einen Export von der Instanz B aufruft und B pausiert. Die Pause bewirkt, dass wir B sofort verlassen und Promise zurückgeben.

Wenn der Aufrufer JavaScript ist, ist dies als dynamische Sprache weniger ein Problem, und tatsächlich ist es vernünftig, vom Aufrufer zu erwarten, dass er den Typ überprüft, wie zuvor besprochen. Wenn der Aufrufer WebAssembly ist, das statisch typisiert ist, ist dies umständlich. Wenn wir im Vorschlag nichts dafür tun, wird der Wert gecastet, in unserem Beispiel von einem Promise zu einer beliebigen Instanz, die A erwartet (wenn ein i32 , würde es gecastet werden ein 0 ). Stattdessen schlagen wir vor, dass ein Fehler auftritt:

  • Wenn eine wasm-Instanz (direkt oder mit call_indirect ) eine Funktion von einer anderen wasm-Instanz aufruft und während der Ausführung in der anderen Instanz ein await ausgeführt wird, dann ist eine RuntimeError -Ausnahme von der Position der await geworfen.

Wichtig ist, dass dies ohne Overhead erfolgen könnte, es sei denn, es wird eine Pause eingelegt, dh normale wasm instance -> wasm instance -Aufrufe werden bei voller Geschwindigkeit gehalten, indem der Stack nur während einer Pause überprüft wird.

Beachten Sie, dass Benutzer, die möchten, dass so etwas wie eine Wasm-Instanz eine andere aufruft und letztere pausiert, dies tun können, aber sie müssen etwas JS zwischen den beiden hinzufügen.

Eine weitere Option hier ist, dass eine Pause auch an das aufrufende Wasm weitergegeben wird, d. h. alle Wasms würden bis zu JS pausieren und möglicherweise mehrere Wasm-Instanzen umfassen. Dies hat einige Vorteile, wie z. B. dass Wasm-Modulgrenzen keine Rolle mehr spielen, aber auch Nachteile, wie die weniger intuitive Weitergabe (der Autor der aufrufenden Instanz erwartet ein solches Verhalten möglicherweise nicht) und dass das Hinzufügen von JS in der Mitte das Verhalten ändern könnte (auch möglicherweise unerwartet). Zu verlangen, dass Benutzer zwischendurch JS haben, wie bereits erwähnt, scheint weniger riskant zu sein.

Eine andere Option könnte darin bestehen, dass einige Wasm-Exporte als asynchron markiert werden, während andere dies nicht sind, und dann könnten wir statisch wissen, was was ist, und keine falschen Aufrufe zulassen. aber sehen Sie sich das event_loop_iteration -Beispiel von früher an, was ein häufiger Fall ist, der nicht durch das Markieren von Exporten gelöst werden würde, und es gibt auch indirekte Aufrufe, sodass wir das Problem auf diese Weise nicht vermeiden können.

Alternative Ansätze in Erwägung gezogen

Vielleicht brauchen wir überhaupt keine neue await -Anweisung, wenn wasm pausiert, wenn ein JS-Import ein Promise zurückgibt? Das Problem ist, dass gerade jetzt, wenn JS ein Promise zurückgibt, kein Fehler vorliegt. Eine solche rückwärtsinkompatible Änderung würde bedeuten, dass wasm kein Promise mehr ohne Pause erhalten kann, aber das könnte auch nützlich sein.

Eine andere Option, die wir in Betracht gezogen haben, besteht darin, Importe irgendwie zu markieren, um zu sagen: "Dieser Import sollte anhalten, wenn er ein Versprechen zurückgibt". Wir haben über verschiedene Optionen nachgedacht, wie man sie markieren kann, entweder auf der JS- oder der Wasm-Seite, aber nichts gefunden, was sich richtig angefühlt hat. Wenn wir beispielsweise Importe auf der JS-Seite markieren, würde das wasm-Modul nicht wissen, ob ein Aufruf eines Imports pausiert oder nicht, bis der Link-Schritt eintrifft, wenn Importe eintreffen. Das heißt, Aufrufe zum Importieren und Pausieren würden „durchmischt“. Es scheint, als ob es am einfachsten wäre, einfach eine neue Anweisung dafür zu haben, await , die ausdrücklich das Warten betrifft. Theoretisch kann eine solche Funktion auch außerhalb des Webs nützlich sein (siehe Anmerkungen weiter oben), sodass eine Anweisung für alle die Dinge insgesamt konsistenter machen kann.

Frühere verwandte Diskussionen

https://github.com/WebAssembly/design/issues/1171
https://github.com/WebAssembly/design/issues/1252
https://github.com/WebAssembly/design/issues/1294
https://github.com/WebAssembly/design/issues/1321

Vielen Dank fürs Lesen, Feedback ist willkommen!

Hilfreichster Kommentar

Ich hatte gehofft, hier öffentlich mehr Diskussionen zu führen, aber um Zeit zu sparen, habe ich mich direkt an einige VM-Implementierer gewandt, da sich bisher nur wenige hier engagiert haben. Angesichts ihres Feedbacks zusammen mit der Diskussion hier denke ich leider, dass wir diesen Vorschlag pausieren sollten.

Await hat ein viel einfacheres beobachtbares Verhalten als allgemeine Coroutinen oder Stapelwechsel, aber die VM-Leute, mit denen ich gesprochen habe, stimmen @rossberg zu, dass die VM-Arbeit am Ende wahrscheinlich für beide ähnlich sein würde. Und zumindest einige VM-Leute glauben, dass wir sowieso Coroutinen oder Stack-Switching bekommen werden und dass wir damit die Anwendungsfälle von await unterstützen können. Das bedeutet, dass bei jedem Aufruf in wasm eine neue Coroutine/ein neuer Stack erstellt wird (anders als bei diesem Vorschlag), aber zumindest einige VM-Leute denken, dass dies schnell genug gemacht werden könnte.

Zusätzlich zu dem mangelnden Interesse von VM-Leuten hatten wir einige starke Einwände gegen diesen Vorschlag hier von @fgmccabe und @RossTate , wie oben besprochen. Wir sind uns in einigen Dingen nicht einig, aber ich schätze diese Standpunkte und die Zeit, die aufgewendet wurde, um sie zu erklären.

Zusammenfassend fühlt es sich insgesamt so an, als wäre es Zeitverschwendung, zu versuchen, hier voranzukommen. Aber danke an alle, die sich an der Diskussion beteiligt haben! Und hoffentlich motiviert dies zumindest zur Priorisierung von Coroutinen / Stapelwechseln.

Beachten Sie, dass der JS-Teil dieses Vorschlags in Zukunft relevant sein kann, da JS-Zucker grundsätzlich für eine bequeme Promise-Integration dient. Wir müssen auf Stapelwechsel oder Koroutinen warten und sehen, ob dies darüber hinaus funktionieren könnte. Aber ich glaube nicht, dass es sich lohnt, das Thema dafür offen zu halten, also schließe ich es.

Alle 96 Kommentare

Hervorragende Aufmachung! Ich mag die Idee der hostgesteuerten Suspendierung. Der Vorschlag von @rossberg behandelt auch funktionale Effektsysteme, und ich bin zugegebenermaßen kein Experte für sie, aber auf den ersten Blick scheint es, als könnten diese denselben Bedarf an nicht-lokalem Kontrollfluss erfüllen.

Bezüglich: "Angesichts des Obigen besteht eine natürliche Implementierung darin, den Stapel zu kopieren, wenn wir anhalten." Wie würde dies für den Ausführungsstapel funktionieren? Ich stelle mir vor, dass die meisten JIT-Engines den nativen C-Ausführungsstapel zwischen JS und Wasm teilen, daher bin ich mir nicht sicher, was Speichern und Wiederherstellen in diesem Zusammenhang bedeuten würde. Bedeutet dieser Vorschlag, dass der Wasm-Ausführungsstapel irgendwie virtualisiert werden müsste? IIUC, die Verwendung des C-Stacks auf diese Weise zu vermeiden, war ziemlich schwierig, als Python versuchte, etwas Ähnliches zu tun: https://github.com/stackless-dev/stackless/wiki.

Ich teile eine ähnliche Sorge wie @sbc100. Das Kopieren des Stapels ist von Natur aus ein ziemlich schwieriger Vorgang, insbesondere wenn Ihre VM nicht bereits über eine GC-Implementierung verfügt.

@sbc100

Bedeutet dieser Vorschlag, dass der Wasm-Ausführungsstapel irgendwie virtualisiert werden müsste?

Ich muss dies den VM-Implementierern überlassen, da ich kein Experte dafür bin. Und ich verstehe die Verbindung zu stapellosem Python nicht, aber vielleicht weiß ich nicht, was das ist, gut genug, um die Verbindung zu verstehen, sorry!

Aber im Allgemeinen: Verschiedene Coroutine-Ansätze funktionieren, indem sie den Stapelzeiger auf niedriger Ebene manipulieren . Diese Ansätze können hier eine Option sein. Wir wollten darauf hinweisen, dass selbst wenn der Stack im Rahmen eines solchen Ansatzes kopiert werden muss, dies in diesem Zusammenhang einen akzeptablen Overhead hat.

(Wir sind uns nicht sicher, ob diese Ansätze in Wasm-VMs funktionieren können oder nicht – wir hoffen, von Implementierern zu hören, ob ja oder nein, und ob es bessere Optionen gibt!)

@lachlansneff

Können Sie bitte genauer erklären, was Sie unter GC verstehen, das die Dinge einfacher macht? Ich folge nicht.

@kripken GCs haben oft (aber nicht immer) die Fähigkeit, einen Stack zu durchlaufen, was notwendig ist, wenn Sie Zeiger auf dem Stack neu schreiben müssen, um auf den neuen Stack zu zeigen. Vielleicht kann das jemand, der mehr über JSC weiß, bestätigen oder dementieren.

@lachlansneff

Danke, jetzt verstehe ich, was du meinst.

Wir schlagen nicht vor, dass es notwendig ist, den Stapel so vollständig zu durchlaufen (jeden Einheimischen ganz oben zu identifizieren usw.), um dies zu tun. (Weitere mögliche Ansätze finden Sie unter dem Link in meinem letzten Kommentar zu Low-Level-Koroutinen-Implementierungsmethoden.)

Ich entschuldige mich für die Terminologie von "Copy the Stack" in dem Vorschlag - ich sehe, dass es nicht klar genug war, basierend auf Ihrem und dem Feedback von @ sbc100 . Auch hier möchten wir keinen bestimmten VM-Implementierungsansatz vorschlagen. Wir wollten nur sagen, dass, wenn das Kopieren des Stacks bei irgendeinem Ansatz notwendig ist, dies kein Problem für die Geschwindigkeit wäre.

Anstatt einen bestimmten Implementierungsansatz vorzuschlagen, hoffen wir, von VM-Leuten zu hören, wie dies ihrer Meinung nach durchgeführt werden könnte!

Ich bin sehr gespannt auf diesen Vorschlag. Lucet hat jetzt schon seit einiger Zeit die Operatoren yield und resume , und wir verwenden sie genau für die Interaktion mit asynchronem Code, der in der Rust-Hostumgebung ausgeführt wird.

Dies war ziemlich einfach zu Lucet hinzuzufügen, da unser Design bereits dazu verpflichtet war, einen separaten Stack für die Wasm-Ausführung beizubehalten, aber ich könnte mir vorstellen, dass es einige Implementierungsschwierigkeiten für VMs geben könnte, die dies nicht tun.

Dieser Vorschlag klingt großartig! Wir haben eine Weile versucht, einen guten Weg für die Verwaltung von asynchronem Code auf wasmer-js zu finden (da wir keinen Zugriff auf die Interna der VM im Browserkontext haben).

Anstatt einen bestimmten Implementierungsansatz vorzuschlagen, hoffen wir, von VM-Leuten zu hören, wie dies ihrer Meinung nach durchgeführt werden könnte!

Ich denke, dass die Verwendung der Callback-Strategie für asynchrone Funktionen vielleicht der einfachste Weg ist, die Dinge ins Rollen zu bringen, und zwar auf sprachunabhängige Weise.

Es scheint, dass .await in einer JsPromise innerhalb einer Rust-Funktion mit wasm-bindgen-futures aufgerufen werden kann. Wie kann das ohne die hier vorgeschlagene await Anweisung funktionieren? Es tut mir leid für meine Unwissenheit, ich suche nach Lösungen, um fetch in wasm aufzurufen, und ich lerne etwas über Asyncify, aber es scheint, dass die Rust-Lösung einfacher ist. Was fehlt mir hier? Kann mir das jemand klar machen?

Ich freue mich sehr über diesen Vorschlag. Der Hauptvorteil des Vorschlags ist seine Einfachheit, da wir APIs erstellen können, die mit dem POV von wasm synchronisiert sind, und es macht es viel einfacher, Anwendungen zu portieren, ohne explizit über Callbacks und async/await nachdenken zu müssen. Es würde uns ermöglichen, WASM- und WebGPU-basiertes maschinelles Lernen mithilfe einer einzigen nativen API auf native Wasm-VMs zu bringen und sowohl im Web als auch nativ auszuführen.

Eine Sache, die ich für diskussionswürdig halte, ist die Signatur der Funktionen, die möglicherweise auf Aufrufe warten. Stellen Sie sich vor, wir haben die folgende Funktion

int test() {
   await();
   return 1;
}

Die Signatur der entsprechenden Funktion ist () => i32 . Unter dem neuen Vorschlag könnten Testaufrufe entweder i32 oder Promise<i32> zurückgeben. Beachten Sie, dass es schwieriger ist, den Benutzer zu bitten, eine neue Signatur statisch zu deklarieren (weil die Kosten für die Codeportierung und indirekte Aufrufe innerhalb der Funktion sein könnten, von denen wir nicht wissen, dass Aufrufe warten).

Sollten wir einen separaten Aufrufmodus in der exportierten Funktion (z. B. asynchroner Aufruf) haben, um anzuzeigen, dass await während der Laufzeit zulässig ist?

Terminologisch ist die vorgeschlagene Operation wie eine Yield-Operation in Betriebssystemen. Da es die Kontrolle an das Betriebssystem (in diesem Fall die Wasm-VM) abgibt, um auf das Ende des Systemaufrufs zu warten.

Wenn ich diesen Vorschlag richtig verstehe, ist er meiner Meinung nach ungefähr gleichbedeutend mit der Aufhebung der Einschränkung, dass await in JS nur in async -Funktionen verwendet werden kann. Das heißt, auf der Wasm-Seite könnte waitref externref sein und anstelle einer await Anweisung könnten Sie eine importierte Funktion $await : [externref] -> [] haben, und auf der JS-Seite Sie könnten foo(promise) => await promise als zu importierende Funktion angeben. In der anderen Richtung, wenn Sie JS-Code wären, der await auf ein Promise außerhalb der async -Funktion setzen wollte, könnten Sie dieses Promise an ein Wasm-Modul übergeben, das einfach await aufruft am Eingang. Ist das ein richtiges Verständnis?

@RossTate Nicht ganz, AIUI. Der Wasm-Code kann await ein Versprechen geben (nennen Sie es promise1 ), aber nur die Wasm-Ausführung wird nachgeben, nicht das JS. Der Wasm-Code gibt ein anderes Versprechen (nennen Sie es promise2 ) an den JS-Aufrufer zurück. Wenn promise1 aufgelöst wird, wird die Wasm-Ausführung fortgesetzt. Wenn dieser Wasm-Code schließlich normal beendet wird, wird promise2 mit dem Ergebnis der Wasm-Funktion aufgelöst.

@tqchen

Sollten wir einen separaten Aufrufmodus in der exportierten Funktion (z. B. asynchroner Aufruf) haben, um anzuzeigen, dass await während der Laufzeit zulässig ist?

Interessant - wo sehen Sie den Nutzen? Wie Sie sagten, gibt es in üblichen Portierungssituationen wirklich keine Möglichkeit, sicher zu sein, ob ein Export am Ende ein await macht oder nicht, also könnte es bestenfalls nur manchmal verwendet werden. Würde dies vielleicht VMs intern helfen?

Durch eine explizite Deklaration kann sichergestellt werden, dass der Benutzer seine Absicht klar zum Ausdruck bringt, und die VM könnte eine ordnungsgemäße Fehlermeldung ausgeben, wenn die Absicht des Benutzers keinen Aufruf ausführt, der asynchron ausgeführt wird.

Aus Sicht des Benutzers macht es auch das Schreiben von Code konsistenter. Beispielsweise könnte der Benutzer den folgenden Code schreiben, selbst wenn test kein await aufruft und die Systemschnittstelle Promise.resolve(test()) automatisch zurückgibt.

await inst.exports_async.test();

Aus Sicht des Benutzers macht es auch das Schreiben von Code konsistenter. Beispielsweise könnte der Benutzer den folgenden Code schreiben, selbst wenn test kein await aufruft und die Systemschnittstelle Promise.resolve(test()) automatisch zurückgibt.

@tqchen Beachten Sie, dass der Benutzer dies bereits tun kann, wie im Beispiel im Vorschlagstest gezeigt. Das heißt, JavaScript unterstützt und verarbeitet bereits synchrone und asynchrone Werte in einem await -Operator auf die gleiche Weise.

Wenn der Vorschlag darin besteht, einen einzelnen statischen Typ zu erzwingen , dann glauben wir, dass dies entweder auf Lint- oder Typsystemebene oder auf einer JavaScript-Wrapper-Ebene erfolgen kann, ohne die Komplexität auf der zentralen WebAssembly-Seite einzuführen oder Implementierer solcher Wrapper einzuschränken.

Ah, danke für die Korrektur, @binji.

Ist in diesem Fall Folgendes ungefähr äquivalent? Fügen Sie der JS-API eine WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") -Funktion hinzu. Angenommen, moduleBytes hat eine Reihe von Importen plus einen zusätzlichen Import import "name1" "name2" (func (param externref)) . Dann instanziiert diese Funktion die Importe mit den durch imports gegebenen Werten und instanziiert den zusätzlichen Import mit dem, was konzeptionell await ist. Wenn exportierte Funktionen aus diesem Modul erstellt werden, werden sie bewacht, sodass es beim Aufruf await den Stack hinaufgeht, um den ersten Wächter zu finden, und dann den Inhalt des Stacks in ein neues Promise kopiert, das dann ist sofort zurückgegeben.

Funktioniert das? Meiner Meinung nach kann dieser Vorschlag nur durch Ändern der JS-API erfolgen, ohne dass WebAssembly selbst geändert werden muss. Natürlich werden auch dann noch viele nützliche Funktionen hinzugefügt.

@kripken Wie würde die Funktion start gehandhabt? Würde es await statisch verbieten, oder würde es irgendwie mit der Wasm-Instanziierung interagieren?

@malbarbo wasm-bindgen-futures ermöglicht es Ihnen, async -Code in Rust auszuführen. Das bedeutet, dass Sie Ihr Programm asynchron schreiben müssen: Sie müssen Ihre Funktionen als async markieren und .await verwenden. Mit diesem Vorschlag können Sie jedoch asynchronen Code ausführen, ohne async oder .await zu verwenden, stattdessen sieht es aus wie ein normaler synchroner Funktionsaufruf.

Mit anderen Worten, Sie können derzeit keine synchronen Betriebssystem-APIs (wie std::fs ) verwenden, da das Web nur über asynchrone APIs verfügt. Aber mit diesem Vorschlag könnten Sie synchrone Betriebssystem-APIs verwenden: Sie würden intern Promises verwenden, aber sie würden synchron zu Rust aussehen .

Selbst wenn dieser Vorschlag implementiert wird, wird wasm-bindgen-futures immer noch existieren und weiterhin nützlich sein, da es einen anderen Anwendungsfall behandelt (Ausführen async -Funktionen). Und async Funktionen sind nützlich, weil sie leicht parallelisiert werden können.

@RossTate Es scheint, dass Ihr Vorschlag dem in "Alternative Ansätze in Betracht gezogenen" ziemlich ähnlich ist:

Eine andere Option, die wir in Betracht gezogen haben, besteht darin, Importe irgendwie zu markieren, um zu sagen: "Dieser Import sollte anhalten, wenn er ein Versprechen zurückgibt". Wir haben über verschiedene Optionen nachgedacht, wie man sie markieren kann, entweder auf der JS- oder der Wasm-Seite, aber nichts gefunden, was sich richtig angefühlt hat. Wenn wir beispielsweise Importe auf der JS-Seite markieren, würde das wasm-Modul nicht wissen, ob ein Aufruf eines Imports pausiert oder nicht, bis der Link-Schritt eintrifft, wenn Importe eintreffen. Das heißt, Aufrufe zum Importieren und Pausieren würden „durchmischt“. Es scheint, als wäre es am einfachsten, einfach eine neue Anweisung dafür zu haben, await, die sich explizit auf das Warten bezieht. Theoretisch kann eine solche Funktion auch außerhalb des Webs nützlich sein (siehe Anmerkungen weiter oben), sodass eine Anweisung für alle die Dinge insgesamt konsistenter machen kann.

Wie würde die Startfunktion gehandhabt werden? Würde es das Warten statisch verbieten oder würde es irgendwie mit der Wasm-Instanziierung interagieren?

@Pauan Wir haben das nicht speziell behandelt, aber ich denke, nichts hindert uns daran, await auch in start zuzulassen. In diesem Fall würde das von instantiate{Streaming} zurückgegebene Promise immer noch natürlich aufgelöst/zurückgewiesen werden, wenn die Startfunktion die Ausführung vollständig beendet hat, mit dem einzigen Unterschied, dass es auf await ed Promises warten würde.

Allerdings gelten die gleichen Einschränkungen wie heute und es wäre vorerst nicht allzu nützlich für Fälle, die Zugriff auf zB den exportierten Speicher erfordern.

@RReverser Wie würde das für das synchrone new WebAssembly.Instance funktionieren (das in Workern verwendet wird)?

Interessanter Punkt @Pauan zum Thema Start!

Ja, für die synchrone Instanziierung scheint es riskant - wenn await erlaubt ist, ist es seltsam, wenn jemand in die Exporte ruft, während sie angehalten sind. await dort zu verbieten ist vielleicht am einfachsten und sichersten. (Vielleicht auch beim asynchronen Start aus Konsistenzgründen, es scheint keine wichtigen Anwendungsfälle zu geben, die dies verhindern würden? Benötigt mehr Überlegungen.)

(das bei Arbeitnehmern verwendet wird)?

Hm, guter Punkt; Ich glaube nicht, dass es in Workers verwendet werden muss, aber da diese API bereits existiert, könnte sie vielleicht ein Promise zurückgeben? Ich habe dies als ein halbwegs beliebtes aufkommendes Muster gesehen, um Theables von einem Konstruktor verschiedener Bibliotheken zurückzugeben, obwohl ich nicht sicher bin, ob es eine gute Idee ist, dies in einer Standard-API zu tun.

Ich stimme zu, es in start (wie beim Einfangen) nicht zuzulassen, ist vorerst am sichersten, und wir können das in Zukunft jederzeit auf abwärtskompatible Weise ändern, falls sich etwas ändert.

Vielleicht habe ich etwas übersehen, aber es gibt keine Diskussion darüber, was passiert, wenn die WASM-Ausführung mit einer await -Anweisung angehalten und ein Promise an JS zurückgegeben wird, dann ruft JS WASM zurück, ohne auf das Promise zu warten.

Ist das ein gültiger Anwendungsfall? Wenn dies der Fall ist, könnte es "Hauptschleifen" -Anwendungen ermöglichen, Eingabeereignisse zu empfangen, ohne dem Browser manuell nachzugeben. Stattdessen könnten sie nachgeben, indem sie auf ein Versprechen warten, das sofort eingelöst wird.

Was ist mit der Stornierung? Es ist nicht in JS-Promises implementiert und dies verursacht einige Probleme.

@Kangz

Vielleicht habe ich etwas verpasst, aber es gibt keine Diskussion darüber, was passiert, wenn die WASM-Ausführung mit einer await-Anweisung angehalten und ein Promise an JS zurückgegeben wird, dann ruft JS WASM zurück, ohne auf das Promise zu warten.

Ist das ein gültiger Anwendungsfall? Wenn dies der Fall ist, könnte es "Hauptschleifen" -Anwendungen ermöglichen, Eingabeereignisse zu empfangen, ohne dem Browser manuell nachzugeben. Stattdessen könnten sie nachgeben, indem sie auf ein Versprechen warten, das sofort eingelöst wird.

Der derzeitige Text ist diesbezüglich vielleicht nicht klar genug. Für den ersten Absatz, ja, das ist erlaubt, siehe den Abschnitt "Klarstellungen": It is ok to call into the WebAssembly instance while a pause has occurred, and multiple pause/resume events can be in flight at once.

Für den zweiten Absatz, nein - Sie können Ereignisse nicht früher abrufen, und Sie können JS nicht dazu bringen, ein Versprechen früher zu lösen, als dies der Fall wäre. Lassen Sie mich versuchen, die Dinge anders zusammenzufassen:

  • Wenn wasm bei Promise A pausiert, kehrt es zu dem, was es aufgerufen hat, zurück und gibt ein neues Promise B zurück.
  • Wasm wird fortgesetzt, wenn Versprechen A verrechnet wird. Das geschieht zur normalen Zeit , was bedeutet, dass in der JS-Ereignisschleife alles normal ist.
  • Nachdem wasm fortgesetzt und auch beendet wurde, wird Promise B erst dann aufgelöst.

Also muss insbesondere Promise B nach Promise A aufgelöst werden. Sie können das Ergebnis von Promise A nicht früher erhalten als JS es bekommen kann.

Anders ausgedrückt: Das Verhalten dieses Vorschlags kann durch Asyncify + einige JS, die Promises um ihn herum verwenden, polyfilled werden.

@RReverser , ich glaube nicht, dass das gleich ist, aber zuerst denke ich, dass wir etwas klären müssen (falls es noch nicht geklärt wurde, in diesem Fall tut es mir leid, dass ich es verpasst habe).

Es können gleichzeitig mehrere Aufrufe von JS in dieselbe wasm-Instanz auf demselben Stack erfolgen. Wenn await von der Instanz ausgeführt wird, welcher Aufruf wird angehalten und gibt ein Promise zurück?

Für den zweiten Absatz, nein - Sie können Ereignisse nicht früher abrufen, und Sie können JS nicht dazu bringen, ein Versprechen früher zu lösen, als dies der Fall wäre.

Entschuldigung, ich glaube, meine Frage war nicht klar. Im Moment verwenden "Main-Loop"-Apps in C++ emscripten_set_main_loop , damit zwischen jedem Lauf der Frame-Funktion die Kontrolle an den Browser zurückgegeben wird und Eingaben oder andere Ereignisse verarbeitet werden können.

Mit diesem Vorschlag scheint Folgendes zu funktionieren, um "Main-Loop" -Apps zu übersetzen. (obwohl ich die JS-Ereignisschleife nicht gut kenne)

int main() {
  while (true) {
    frame();
    processEvents();
  }
}

// polyfillable with ASYNCIFY!
void processEvents() {
  __builtin_await(EM_ASM(
    new Promise((resolve, reject) => {
      setTimeout(0, () => resolve());
    })
  ))
}

@Kangz Das sollte funktionieren, ja (außer Sie haben ein kleines Problem mit der Reihenfolge der Argumente in Ihrem setTimeout-Code und es könnte vereinfacht werden):

int main() {
  while (true) {
    frame();
    processEvents();
  }
}

// polyfillable with ASYNCIFY!
void processEvents() {
  __builtin_await(EM_ASM_WAITREF(
    return new Promise(resolve => setTimeout(resolve));
  ));
}

Es können gleichzeitig mehrere Aufrufe von JS in dieselbe wasm-Instanz auf demselben Stack erfolgen. Wenn await von der Instanz ausgeführt wird, welcher Aufruf wird angehalten und gibt ein Promise zurück?

Das Innerste. Es ist die Aufgabe des JS-Wrappers, den Rest zu koordinieren, wenn er dies wünscht.

@Kangz Entschuldigung, ich habe dich vorher falsch verstanden. Ja, wie @RReverser sagte, das sollte funktionieren, und es ist ein gutes Beispiel für einen beabsichtigten Anwendungsfall hier!

Wie Sie sagten, ist es mit Asyncify polyfillable, und tatsächlich entspricht es dem gleichen Code wie Asyncify heute, indem __builtin_await durch einen Aufruf von emscripten_sleep(0) ersetzt wird (was ein setTimeout(0) macht). .

Danke, @RReverser , für die Klarstellung. Ich denke, es würde helfen, die Beschreibung umzuformulieren, um zu sagen, dass der (letzte) Aufruf in die Instanz anhält und nicht die Instanz selbst.

In diesem Fall klingt das fast gleichbedeutend mit dem Hinzufügen der folgenden zwei primitiven Funktionen zu JS: promise-on-await(f) und await-for-promise(p) . Ersteres ruft f() , aber wenn während der Ausführung von f() ein Aufruf an await-for-promise(p) erfolgt, gibt es stattdessen ein neues Promise zurück, das die Ausführung fortsetzt, nachdem p aufgelöst wurde und löst sich selbst auf, nachdem diese Ausführung abgeschlossen ist (oder ruft erneut await-for-promise auf). Wenn ein Aufruf von await-for-promise im Kontext mehrerer promise-on-await s erfolgt, gibt die letzte ein Promise zurück. Wenn ein Aufruf von await-for-promise außerhalb von promise-on-await erfolgt, passiert etwas Schlimmes (genau wie wenn der start await $#$ ausführt).

Ist das sinnvoll?

@RossTate Das ist ziemlich nah, ja, und fängt die allgemeine Idee ein. (Aber wie Sie sagten, nur fast gleichwertig, da es nicht zum Polyfill verwendet werden konnte und die spezifische Wasm/JS-Grenzbehandlung fehlt.)

Danke für den Vorschlag, diesen Text umzuformulieren. Ich führe hier eine Liste solcher Notizen aus der Diskussion. (Ich bin mir nicht sicher, ob es sich lohnt, sie auf den ersten Beitrag anzuwenden, da es weniger verwirrend erscheint, sie im Laufe der Zeit nicht zu ändern?)

@RossTate Interessant ... Das gefällt mir! Es macht die asynchrone Natur des Aufrufs explizit ( promise-on-await ist für jeden potenziell asynchronen Aufruf erforderlich) und erfordert keine Änderungen an Wasm. Es macht auch (etwas) Sinn, wenn Sie Wasm aus der Mitte entfernen – wenn promise-on-await await-for-promise direkt aufruft, dann gibt es ein Promise zurück.

@kripken kannst du näher darauf eingehen, warum das anders wäre? Ich verstehe nicht ganz, warum die Wasm/JS-Grenze hier wichtig ist.

@binji Ich meinte nur, dass solche Funktionen in JS Wasm nichts Ähnliches tun lassen würden. Sie als Importe von wasm zu bezeichnen, würde nicht funktionieren. Wir brauchen immer noch eine Möglichkeit, Wasm auf wiederaufnehmbare Weise zur Grenze usw. zu bringen, nicht wahr?

@kripken richtig, ich denke, an diesem Punkt müsste der await-for-promise -Import wie ein intrinsischer Wasm funktionieren.

Mein Gedanke war, dass ein solches Modul, anstatt eine await -Anweisung zu wasm hinzuzufügen, stattdessen await-for-promise importieren und das aufrufen würde. Anstatt die exportierten Funktionen zu ändern, würde der JS-Code sie in ähnlicher Weise innerhalb eines promise-on-await aufrufen. Das bedeutet, dass die JS-Primitive die gesamte Stack-Arbeit erledigen würden, einschließlich des WebAssembly-Stacks. Es wäre auch flexibler, z. B. wenn Sie möchten, könnten Sie dem Modul einen JS-Callback geben, der dann das Modul zurückruft und die äußere Aufrufpause anstelle der inneren Klausel hat – es hängt alles davon ab, ob der JS-Code wählt um den Anruf in promise-on-await zu verpacken oder nicht. Ich glaube nicht, dass Sie etwas an wasm selbst ändern müssen.

Mich würde interessieren, was @syg über diese potenziellen JS-Primitive denkt.

Oh ok, Entschuldigung - ich habe Ihren Kommentar @RossTate als "um sicherzugehen, dass ich es verstehe, lassen Sie es mich so umformulieren und mir sagen, ob das die richtige Form hat" und nicht als konkreten Vorschlag verstanden.

Wenn Sie darüber nachdenken, möchte Ihre Idee nicht nur JS-Frames, sondern auch Wasm anhalten, aber es gibt auch Host-/Browser-Frames. (Der aktuelle Vorschlag vermeidet dies, indem er nur an der Grenze arbeitet, an der er aufgerufen wurde.) Hier ist ein Beispiel:

myList.forEach((item) => {
  .. call something which ends up pausing ..
});

Wenn forEach im Browsercode implementiert ist, bedeutet dies, dass Browserframes angehalten werden. Bedeutsam ist auch, dass das Anhalten in der Mitte einer solchen Schleife und das spätere Fortsetzen eine neue Leistung wäre, die JS ausführen kann, und Ihre Idee würde dies auch für eine normale Schleife zulassen:

for (let i of something) {
  .. call something which ends up pausing ..
}

Und all dies kann merkwürdige Spezifikationsinteraktionen mit async JS-Funktionen haben. Dies alles scheinen große Diskussionen mit Browser- und JS-Leuten zu sein.

Aber das vermeidet auch nur das Hinzufügen await und waitref in die Kern-Wasm-Spezifikation, aber das sind winzige Ergänzungen - da sie nichts in der Kern-Spezifikation tun. Der aktuelle Vorschlag hat bereits 99 % der Komplexität auf der JS-Seite. Und Ihr IIUC-Vorschlag tauscht diese kleine Ergänzung zur Wasm-Spezifikation mit viel größeren Ergänzungen auf der JS-Seite aus - so wird die Webplattform als Ganzes komplexer und unnötig, da dies alles für Wasm ist. Außerdem hat die Definition await in der Kern-Wasm-Spezifikation tatsächlich den Vorteil, dass es außerhalb des Webs nützlich sein kann.

Vielleicht habe ich etwas in Ihrem Vorschlag übersehen, wenn ja, entschuldigen Sie bitte. Insgesamt bin ich neugierig, was Ihre Motivation ist, um zu versuchen, eine Ergänzung der Kern-Wasm-Spezifikation zu vermeiden?

Ich glaube nicht, dass diese Primitive für js viel Sinn machen, und ich denke, dass mehr Wasm-Implementierungen als die in Browsern davon profitieren können. Ich bin immer noch neugierig, warum fortsetzbare Ausnahmen (grob Effekte) diesen Anwendungsfall nicht erfüllen würden.

Mein Kommentar war eine Kombination aus beidem. Auf hoher Ebene versuche ich herauszufinden, ob es eine Möglichkeit gibt, den Vorschlag als reine Bereicherung der JS-API umzuformulieren (und ähnlich wie andere Hosts mit wasm-Modulen interagieren würden). Die Übung hilft bei der Beurteilung, ob Wasm wirklich geändert werden muss, und hilft festzustellen, ob der Vorschlag wirklich darin besteht, JS heimlich neue Primitive hinzuzufügen, die JS-Leute billigen oder nicht. Das heißt, wenn es nicht möglich ist, nur ein importiertes await : func (param externref) (result externref) zu verwenden, dann ist es sehr wahrscheinlich, dass JS dadurch neue Funktionen hinzugefügt werden.

Was die Einfachheit der Änderungen an wasm betrifft, gibt es noch viele Dinge zu beachten, wie zum Beispiel, was mit Modul-zu-Modul-Aufrufen zu tun ist, was zu tun ist, wenn exportierte Funktionen GC-Werte zurückgeben, die Zeiger auf Funktionen enthalten, die await ausführen können nach Beendigung des Anrufs usw.

Um auf die Übung zurückzukommen, wie Sie bereits gesagt haben, gibt es gute Gründe, nur den Wasm-Stack zu erfassen. Dies bringt mich zurück zu meinem früheren Vorschlag, wenn auch leicht überarbeitet mit einer neuen Perspektive. Fügen Sie der JS-API eine WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") -Funktion hinzu. Angenommen, moduleBytes hat eine Reihe von Importen plus einen zusätzlichen Import import "name1" "name2" (func (param externref) (result externref)) . Dann instanziiert instantiateAsync die anderen Importe von moduleBytes einfach mit den Werten von imports und instanziiert den zusätzlichen Import mit dem, was konzeptionell await-for-promise ist. Wenn exportierte Funktionen aus dieser Instanz erstellt werden, werden sie geschützt (konzeptionell durch promise-on-await ), sodass beim Aufruf await-for-promise der Stapel nach oben geht, um den ersten Wächter zu finden, und dann den Inhalt von kopiert den Stapel in ein neues Promise umwandeln, das dann sofort zurückgegeben wird. Jetzt haben wir dieselben Primitive wie oben erwähnt, aber sie sind nicht mehr erstklassig, und dieses eingeschränkte Muster stellt sicher, dass nur Wasm-Stapel jemals erfasst werden. Gleichzeitig muss WebAssembly nicht geändert werden, um das Muster zu unterstützen.

Gedanken?

@devsnek

Ich bin immer noch neugierig, warum fortsetzbare Ausnahmen (grob Effekte) diesen Anwendungsfall nicht erfüllen würden.

Sie sind eine Option in diesem Bereich, sicher.

Mein Verständnis von @rossbergs letzter Präsentation ist, dass er ursprünglich diesen Weg gehen wollte, dann aber die Richtung geändert hat, um einen Coroutine-Ansatz zu verfolgen. Siehe die Folie mit dem Titel "Probleme". Nach dieser Folie werden Coroutinen beschrieben, die eine weitere Option in diesem Bereich darstellen. Also vielleicht ist deine Frage eher für @rossberg , wer kann das vielleicht klären?

Dieser Vorschlag konzentriert sich auf die Lösung des Sync/Async-Problems, das nicht so viel Leistung erfordert wie wiederaufnehmbare Ausnahmen oder Coroutinen. Diese konzentrieren sich auf interne Interaktionen innerhalb eines wasm-Moduls, während wir uns auf die Interaktion zwischen einem wasm-Modul und der Umgebung konzentrieren (weil dort das Sync/Async-Problem auftritt). Aus diesem Grund brauchen wir nur eine einzige neue Anweisung in der Kern-Wasm-Spezifikation, und fast die gesamte Logik in diesem Vorschlag befindet sich in der Wasm-JS-Spezifikation. Und es bedeutet, dass Sie auf ein Versprechen wie dieses warten können:

call $get_promise
await
;; use it!

Diese Einfachheit im Wasm ist für sich selbst nützlich, bedeutet aber auch, dass es für die VM sehr klar ist, was vor sich geht, was auch Vorteile haben kann.

@RossTate

Das heißt, wenn es nicht möglich ist, nur ein importiertes await : func (param externref) (result externref) zu verwenden, dann ist es sehr wahrscheinlich, dass dies JS neue Funktionen hinzufügt.

Ich kann dieser Schlussfolgerung nicht folgen, sorry. Aber es scheint mir umständlich. Wenn Sie der Meinung sind, dass dieser Vorschlag neue Funktionen zu JS hinzufügt, warum zeigen Sie das nicht direkt? (Ich bin fest davon überzeugt, dass dies nicht der Fall ist, bin aber neugierig, ob Sie feststellen, dass wir einen Fehler gemacht haben!)

Was die Einfachheit der Änderungen an wasm betrifft, gibt es noch viele Dinge zu beachten, wie zum Beispiel, was mit Modul-zu-Modul-Aufrufen zu tun ist

Sagt die Kern-Wasm-Spezifikation irgendetwas über Modul-zu-Modul-Aufrufe aus? Ich erinnere mich nicht, dass es das getan hat, und wenn ich jetzt relevante Abschnitte überfliege, sehe ich das nicht. Aber vielleicht habe ich etwas übersehen?

Meine Überzeugung ist, dass die Kern-Wasm-Spezifikationsergänzungen im Grunde darin bestehen würden, await , zu sagen, dass es "auf etwas warten" soll, und das war's. Deshalb habe ich That's it for the core wasm spec! in den Vorschlag geschrieben. Wenn ich falsch liege, zeigen Sie mir bitte in der Kern-Wasm-Spezifikation, wo wir mehr hinzufügen müssten.

Lassen Sie uns spekulieren und sagen, dass die Kern-Wasm-Spezifikation eines Tages eine neue Anweisung zum Erstellen eines Wasm-Moduls und zum Aufrufen einer Methode dafür enthalten wird. In diesem Fall, denke ich, würden wir await nur Fallen sagen, weil es darauf ankommt, auf etwas außerhalb, auf dem Host, zu warten.

Das bringt mich zurück zu meinem früheren Vorschlag, wenn auch leicht überarbeitet mit einer neuen Perspektive [neue Idee]

Ist diese Idee nicht funktionell dieselbe wie der zweite Absatz in Alternative approaches considered im Vorschlag? So etwas kann getan werden, aber wir haben erklärt, warum wir es für weniger gut halten.

@kripken hat es verstanden. Um es klar zu sagen, ich denke, await löst die vorgestellten Anwendungsfälle auf sehr praktische und elegante Weise. Ich hoffe nur irgendwie, dass wir diese Dynamik vielleicht nutzen können, um auch andere Anwendungsfälle zu lösen, indem wir das Design ein wenig erweitern.

Ich denke, dass der Vorschlag von @RossTate in der Tat sehr nach dem klingt, was in "Alternative Ansätze berücksichtigt" erwähnt wird. Ich denke also, wir sollten ausführlicher diskutieren, warum dieser Ansatz verworfen wurde. Ich denke, wir sind uns alle einig, dass eine Lösung ohne Wasm-Spezifikationsänderungen vorzuziehen wäre, wenn wir die JS-Seite funktionsfähig machen können. Ich versuche, die Nachteile zu verstehen, die Sie in diesem Abschnitt darlegen, und warum sie die reine JS-Lösung so inakzeptabel machen.

Ich denke, wir sind uns alle einig, dass eine Lösung ohne Wasm-Spezifikationsänderungen vorzuziehen wäre

Nein! Sehen Sie sich die hier besprochenen Nicht-Web-Anwendungsfälle an. Ohne await in der wasm-Spezifikation würden wir am Ende damit enden, dass jede Plattform etwas Ad-hoc macht: Die JS-Umgebung führt einige Import-Dinge durch, andere Orte erstellen neue APIs, die als "synchronous" gekennzeichnet sind, usw. Das wasm-Ökosystem würde dies tun weniger konsistent sein, es wäre schwieriger, einen Wasm aus dem Web an andere Orte zu verschieben usw.

Aber ja, wir sollten den Kern-Wasm-Spezifikationsteil so einfach wie möglich machen. Ich denke, das macht das? 99 % der Logik liegt auf der JS-Seite (aber @RossTate scheint anderer Meinung zu sein, und wir versuchen immer noch, das herauszufinden - ich habe in meiner letzten Antwort konkrete Fragen gestellt, von denen ich hoffe, dass sie die Dinge voranbringen).

Meine Überzeugung ist, dass die Kern-Wasm-Spezifikationsergänzungen im Grunde darin bestehen würden, await , zu sagen, dass es "auf etwas warten" soll, und das war's.

Sofern diese Semantik nicht genauer formalisiert werden kann, führt dies Mehrdeutigkeit oder implementierungsdefiniertes Verhalten in die Spezifikation ein. Wir haben das bisher vermieden (zu erheblichen Kosten im Fall von SIMD), also ist dies definitiv etwas, das ich gerne festgenagelt sehen würde. Ich denke nicht, dass der Vorschlag selbst geändert werden muss, um dies formeller zu machen, aber "auf etwas warten" sollte in der genauen Terminologie umformuliert werden, die bereits von der Spezifikation verwendet wird.

Sagt die Kern-Wasm-Spezifikation irgendetwas über Modul-zu-Modul-Aufrufe aus?

Die Importe einer Instanz können mit den Exporten einer anderen Instanz instanziiert werden. Soweit ich die JS-API (und das Kompositionsprinzip von Wasm) verstehe, ist ein Aufruf eines solchen Imports konzeptionell ein direkter Aufruf einer beliebigen Funktion, die die andere Instanz exportiert hat. Dasselbe gilt für (indirekte) Aufrufe von Funktionswerten wie funcref , die zwischen den beiden Instanzen weitergegeben werden.

Lassen Sie uns spekulieren und sagen, dass die Kern-Wasm-Spezifikation eines Tages eine neue Anweisung zum Erstellen eines Wasm-Moduls und zum Aufrufen einer Methode dafür enthalten wird. In diesem Fall, denke ich, würden wir sagen, await just traps, weil es darauf ankommt, auf etwas außerhalb, auf dem Host, zu warten.

Basierend auf dem beim persönlichen Treffen besprochenen Modul-Kompositionalitäts-Prinzip sollte es keine Falle geben. Es sollte so sein, als ob es nur eine (zusammengesetzte) Modulinstanz gäbe und diese await ausführte. Das heißt, await würde den Stack bis zum neuesten JS-Stack-Frame packen.

Beachten Sie, dass dies impliziert, dass, wenn f der Wert einer exportierten unären Funktion einer Wasm-Instanz wäre, das Instanziierungsparameter-Objekt {"some" : {"import" : f}} {"some" : {"import" : (x) => f(x)}} an Ersteres bleibt im Wasm-Stack, während Aufrufe an Letzteres in den JS-Stack gelangen, wenn auch nur knapp. Bisher würden diese Instanziierungsparameter-Objekte als äquivalent betrachtet. Ich kann darauf eingehen, warum das aus Sicht der Codemigration/Sprachinterop nützlich ist, aber das wäre im Moment ein Exkurs.

Ist diese Idee nicht funktional dieselbe wie der zweite Absatz in Alternative Ansätze, die im Vorschlag berücksichtigt werden? So etwas kann getan werden, aber wir haben erklärt, warum wir es für weniger gut halten.

Entschuldigung, ich habe diese Alternative so gelesen, dass sie etwas anderes bedeutet, aber das spielt jetzt keine Rolle, außer um meine Verwirrung zu erklären. Anscheinend meinten Sie dasselbe wie mein Vorschlag, in diesem Fall lohnt es sich, die Vor- und Nachteile zu diskutieren.

Die Tatsache, dass dieser Vorschlag auf der Wasm-Seite so leicht ist, liegt daran, dass die Anweisung await semantisch mit einem Aufruf einer importierten Funktion identisch zu sein scheint. Natürlich sind Konventionen wichtig, wie Sie betonen! Aber await ist nicht die einzige Funktionalität, für die dies gilt; dasselbe gilt für die meisten importierten Funktionen. Im Fall von await ist mein Gefühl, dass die Besorgnis über Konventionen dadurch angegangen werden könnte, dass Module mit dieser Funktionalität eine import "control" "await" (func (param externref) (result externref)) -Klausel haben und dass Umgebungen, die diese Funktionalität unterstützen, diesen Import immer instanziieren mit entsprechendem Rückruf.

Das scheint eine Lösung zu sein, die eine Menge Arbeit spart, indem sie wasm nicht ändert und gleichzeitig die plattformübergreifende Portabilität bietet, nach der Sie suchen. Aber ich arbeite immer noch daran, die Nuancen des Vorschlags zu verstehen, und ich habe bisher schon einiges verpasst!

Die Tatsache, dass dieser Vorschlag auf der Wasm-Seite so leicht ist, liegt daran, dass die await-Anweisung semantisch mit einem Aufruf einer importierten Funktion identisch zu sein scheint.

FWIW, hier hat dieser Vorschlag ursprünglich begonnen, aber die Verwendung solcher Intrinsics scheint für die VMs undurchsichtiger und allgemein entmutigt (ich denke, @binji schlug vor, sich in ursprünglichen Diskussionen davon zu entfernen).

Zum Beispiel könnte etwas wie memory.grow oder atomic.wait entsprechend Ihrer Argumentation auch als import "control" "memory_grow" oder import "control" "atomic_wait" gemacht werden, aber sie sind nicht so, wie sie es tun bieten nicht das gleiche Maß an Interop- und statischen Analysemöglichkeiten (sowohl auf der VM- als auch auf der Tool-Seite) wie echte Anweisungen.

Man könnte argumentieren, dass memory.grow als Anweisung immer noch nützlich ist, wenn der Speicher nicht exportiert wird, aber atomic.wait könnte definitiv außerhalb des Kerns implementiert werden. Tatsächlich ist es await sehr ähnlich, abgesehen von der Ebene, auf der Pause/Fortsetzen auftritt, und der Tatsache, dass await als Funktion viel mehr Magie erfordern würde als atomic.wait da es in der Lage sein muss, mit dem VM-Stack zu interagieren und nicht nur den aktuellen Thread zu blockieren, bis sich ein Wert ändert.

@tlebhaft

"Warten auf etwas" sollte in die genaue Terminologie umformuliert werden, die bereits von der Spezifikation verwendet wird.

Definitiv Ja. Ich kann jetzt einen spezifischeren Text vorschlagen, wenn das hilfreich wäre:

When an await instruction is executed on a waitref, the host environment is requested to do some work. Typically there would be a natural meaning to what that work is based on what a waitref is on a specific host (in particular, waiting for some form of host event), but from the wasm module's point of view, the semantics of an await are similar to a call to an imported host function, that is: we don't know exactly what the host will do, but at least expect to give it certain types and receive certain results; after the instruction executes, global state (the store) may change; and an exception may be thrown.

The behavior of an await from the host's perspective may be very different, however, from a call to an imported host function, and might involve something like pausing and resuming the wasm module. It is for this reason that this instruction is defined. For the instruction to be usable on a particlar host, the host would need to define the proper behavior.

Übrigens, ein weiterer Vergleich, der mir beim Schreiben kam, sind die Ausrichtungshinweise zu Lasten und Speichern. Wasm unterstützt nicht ausgerichtete Lade- und Speichervorgänge, sodass die Hinweise nicht zu unterschiedlichem Verhalten führen können, das vom Wasm-Modul beobachtet werden kann (selbst wenn der Hinweis falsch ist), aber für den Host schlagen sie eine ganz andere Implementierung auf bestimmten Plattformen vor (die möglicherweise effizienter ist). Das ist also ein Beispiel für verschiedene Anweisungen ohne intern beobachtbare unterschiedliche Semantik, wie die Spezifikation sagt: The alignment in load and store instructions does not affect the semantics .

@RossTate

Basierend auf dem beim persönlichen Treffen besprochenen Modul-Kompositionalitäts-Prinzip sollte es keine Falle geben. Es sollte so sein, als gäbe es nur eine (zusammengesetzte) Modulinstanz, die await ausgeführt wird. Das heißt, await würde den Stack bis zum neuesten JS-Stack-Frame packen.

Klingt gut und gut zu wissen, danke, diesen Teil habe ich verpasst.

Ich denke, das erklärt mir einen Teil unseres Missverständnisses. Modul => Modulaufrufe sind nicht in der wasm-Spezifikation atm, worauf ich vorhin hingewiesen habe. Aber es hört sich so an, als würden Sie sich auf eine zukünftige Spezifikation freuen, in der sie sich befinden könnten. Auf jeden Fall sieht das hier nicht nach einem Problem aus, da die Kompositionalität genau bestimmt, wie sich ein await in dieser Situation verhalten soll (was ich zuvor nicht vorgeschlagen habe!, aber sinnvoller ist).

Sagt die Kern-Wasm-Spezifikation irgendetwas über Modul-zu-Modul-Aufrufe aus? Ich erinnere mich nicht, dass es das getan hat, und wenn ich jetzt relevante Abschnitte überfliege, sehe ich das nicht. Aber vielleicht habe ich etwas übersehen?

Ja, die wasm-Kernspezifikation unterscheidet zwischen Funktionen, die aus anderen wasm-Modulen importiert wurden, und Host-Funktionen (§ 4.2.6). Die Semantik des Funktionsaufrufs (§ 4.4.7) hängt nicht von dem Modul ab, das die Funktion definiert hat, und insbesondere sind modulübergreifende Funktionsaufrufe derzeit so spezifiziert, dass sie sich identisch mit Funktionsaufrufen desselben Moduls verhalten.

Wenn await s unter modulübergreifenden Aufrufen als Trap definiert sind, würde dies die Angabe einer Traversierung des Aufrufstapels nach oben erfordern, um zu prüfen, ob ein modulübergreifender Aufruf vor dem letzten Dummy-Frame existiert, der durch einen Aufruf vom Host erstellt wurde (§ 4.5.5). Dies wäre eine unglückliche Komplikation in der Spezifikation. Aber ich stimme Ross zu, dass modulübergreifende Aufrufe eine Verletzung der Kompositionalität darstellen würden, daher würde ich die Semantik bevorzugen, bei der der gesamte Stapel auf den letzten Aufruf vom Host zurückgefroren wird. Der einfachste Weg, dies zu spezifizieren, wäre, await einem Host-Funktionsaufruf (§ 4.4.7.3) ähnlich zu machen, wie Sie sagen, @kripken. Host-Funktionsaufrufe sind jedoch völlig nicht deterministisch, daher könnte ein besserer Name für die Anweisung aus Sicht der Kernspezifikation undefined sein. Und an diesem Punkt bevorzuge ich tatsächlich einen intrinsischen Import, der immer von der Webplattform (und WASI für die Portabilität) bereitgestellt wird, da die Kernspezifikation allein nicht von einer undefined -Anweisung profitiert, IMO.

Semantisch gesehen ist ein Aufruf an die Hostumgebung, der ein waitref plus ein await zurückgibt, nur ein blockierender Aufruf, richtig?

Welchen Wert bietet dies für Nicht-Web-Einbettungen, die keine asynchrone Umgebung wie ein Browser haben und das Blockieren von Anrufen nativ unterstützen können?

@RReverser , ich verstehe den Punkt, den Sie zu Intrinsics machen. Bei der Entscheidung, wann eine Operation durch nicht interpretierte Funktionen oder Anweisungen definiert werden sollte, ist eine Ermessensentscheidung beteiligt. Ich denke, ein Faktor bei diesem Urteil ist zu berücksichtigen, wie es mit anderen Anweisungen interagiert. memory.grow beeinflusst das Verhalten anderer Speicherbefehle. Ich hatte keine Gelegenheit, den Threads-Vorschlag zu lesen, aber ich kann mir vorstellen, dass atomic.wait das Verhalten anderer Synchronisierungsanweisungen beeinflusst oder von ihnen beeinflusst wird. Die Spezifikation muss dann aktualisiert werden, um diese Interaktionen zu formalisieren.

Aber mit await allein gibt es keine Interaktionen mit anderen Anweisungen. Die einzigen Interaktionen finden mit dem Host statt, weshalb ich der Meinung bin, dass dieser Vorschlag über importierte Hostfunktionen erfolgen sollte.

Ich denke, ein großer Unterschied zwischen atomic.wait und diesem vorgeschlagenen await ist, dass das Modul nicht mit atomic.wait erneut eingegeben werden kann. Der Agent wird vollständig suspendiert.

@kripken :

Mein Verständnis aus der letzten Präsentation von @rossberg ist, dass er ursprünglich diesen Weg gehen wollte, dann aber die Richtung geändert hat, um einen Coroutine-Ansatz zu verfolgen. Siehe die Folie mit dem Titel "Probleme". Nach dieser Folie werden Coroutinen beschrieben, die eine weitere Option in diesem Bereich darstellen. Also vielleicht ist deine Frage eher für @rossberg , wer kann das vielleicht klären?

Ja, also kann man sich die Coroutine-ähnliche Faktorisierung als eine Verallgemeinerung des vorherigen Designs für fortsetzbare Ausnahmen vorstellen. Es hat immer noch die gleiche Vorstellung von fortsetzbaren Ereignissen/Ausnahmen, aber die Anweisung try wird in kleinere Grundelemente zerlegt – was die Semantik einfacher und das Kostenmodell expliziter macht. Es ist auch etwas ausdrucksstärker.

Die Absicht ist immer noch, dass dies alle relevanten Kontrollabstraktionen ausdrücken kann, und Async ist einer der motivierenden Anwendungsfälle. Um mit asynchronem JS zu interagieren, könnte die JS-API vermutlich ein vordefiniertes await -Ereignis bereitstellen (das ein JS-Versprechen als externref trägt), das ein Wasm-Modul importieren und throw aussetzen könnte. Natürlich müssten noch viele Details ausgearbeitet werden, aber im Prinzip sollte das möglich sein.

Was den aktuellen Vorschlag betrifft, versuche ich immer noch, mich darum zu kümmern. :)

Insbesondere scheint es await in jeder alten Wasm-Funktion zuzulassen, lese ich das richtig? Wenn ja, unterscheidet sich das stark von JS, das await nur in asynchronen Funktionen zulässt. Und das ist eine sehr zentrale Einschränkung, weil es Engines ermöglicht, await durch _lokale_ Transformation einer einzelnen (asynchronen) Funktion zu kompilieren!

Ohne diese Einschränkung müssten Engines entweder eine _globale_ Programmtransformation durchführen (wie es angeblich Asyncify tut), wodurch jeder Aufruf potenziell viel teurer werden würde (man kann im Allgemeinen nicht wissen, ob ein Aufruf ein await erreicht). Oder äquivalent dazu müssten Engines in der Lage sein, mehrere Stacks zu erstellen und zwischen ihnen zu wechseln!

Nun, dies ist genau das Feature, das die Coroutinen-/Effekt-Handler-Idee versucht, in Wasm einzuführen. Aber offensichtlich ist es eine höchst nicht triviale Ergänzung der Plattform und ihres Ausführungsmodells, eine Komplikation, die JS wegen seiner Kontrollabstraktionen (wie Async und Generatoren) sehr sorgfältig vermieden hat.

@rossberg

Insbesondere scheint es das Warten in jeder alten Wasm-Funktion zu ermöglichen, lese ich das richtig? Wenn ja, unterscheidet sich das stark von JS, das das Warten nur in asynchronen Funktionen zulässt.

Ja, das Modell hier ist ganz anders. JS await ist funktionsbedingt, während dieser Vorschlag eine ganze wasm-Instanz erwartet (weil das Ziel darin besteht, die Sync/Async-Diskrepanz zwischen JS und wasm zu lösen, die zwischen JS und wasm besteht). Auch JS await ist für handgeschriebenen Code, während dies die Portierung von kompiliertem Code ermöglichen soll.

Und das ist eine sehr zentrale Einschränkung, da es Engines ermöglicht, durch lokale Transformation einer einzelnen (asynchronen) Funktion zu kompilieren! Ohne diese Einschränkung müssten Engines entweder eine globale Programmtransformation durchführen (wie es angeblich Asyncify tut), wodurch jeder Aufruf potenziell viel teurer werden würde (man kann im Allgemeinen nicht wissen, ob ein Aufruf ein await erreicht). Oder äquivalent dazu müssten Engines in der Lage sein, mehrere Stacks zu erstellen und zwischen ihnen zu wechseln!

Eine globale Programmtransformation ist hier definitiv nicht beabsichtigt! Entschuldigung, wenn das nicht klar war.

Wie im Vorschlag erwähnt, ist das Wechseln zwischen Stapeln eine mögliche Implementierungsoption, aber beachten Sie, dass es nicht dasselbe ist wie das Wechseln des Stapels im Coroutine-Stil:

  • Nur die gesamte wasm-Instanz kann pausieren. Dies dient nicht zum Stack-Switching innerhalb des Moduls. (Das ist insbesondere der Grund, warum dieser Vorschlag keine Ergänzungen zur Kern-Wasm-Spezifikation enthalten und vollständig auf der Wasm-JS-Seite sein könnte; bisher bevorzugen einige Leute das, und ich denke, beide Wege können funktionieren.)
  • Coroutinen deklarieren Stacks explizit, await nicht.
  • await stacks können nur einmal fortgesetzt werden, es gibt kein Forking / Returning mehr als einmal (nicht sicher, ob Sie das in Ihrem Vorschlag haben werden oder nicht?).
  • Das Leistungsmodell ist hier sehr unterschiedlich. await wird auf ein Promise in JS warten, das bereits minimalen Overhead und minimale Latenz hat. Es ist also in Ordnung, wenn die Implementierung etwas Overhead hat, wenn wir tatsächlich pausieren, und wir kümmern uns weniger darum, als es Coroutinen wahrscheinlich tun würden.

Angesichts dieser Faktoren und des beobachtbaren Verhaltens dieses Vorschlags, dass eine ganze Wasm-Instanz pausiert, kann es verschiedene Möglichkeiten geben, dies zu implementieren. Beispielsweise könnte außerhalb des Webs in einer VM, auf der eine einzelne Wasm-Instanz ausgeführt wird, diese buchstäblich nur ihre Ereignisschleife ausführen, bis es Zeit ist, das Wasm fortzusetzen. Im Web könnte ein Implementierungsansatz sein: Wenn ein await auftritt, kopieren Sie den gesamten wasm-Stack von der aktuellen Position bis zu der Stelle, an der wir wasm aufgerufen haben; sparen Sie das auf der Seite; Um fortzufahren, kopieren Sie es wieder hinein und fahren Sie einfach von dort fort. Es kann auch andere Ansätze oder Variationen davon geben (einige vielleicht ohne Kopieren, aber auch hier ist das Vermeiden des Kopieraufwands nicht wirklich entscheidend!).

Entschuldigung für den langen Beitrag und einige Wiederholungen aus dem Vorschlagstext selbst, aber ich hoffe, dies hilft, einige der Punkte zu verdeutlichen, auf die Sie sich bezogen haben?

Ich denke, es gibt hier viel zu diskutieren in Bezug auf die Umsetzung. Bisher ist der Kommentar von @acfoltzer über Lucet ermutigend!

Nur um einige Formulierungen in @kripkens letztem Kommentar zu verdeutlichen, es ist nicht die gesamte Wasm-Instanz, die pausiert. Es ist nur der letzte Aufruf von einem Host-Frame in wasm auf dem Stack, der angehalten wird, und dann wird dem Host-Frame stattdessen ein entsprechendes Promise (oder das entsprechende Analogon für den Host) zurückgegeben. Siehe hier für die relevante frühere Klarstellung.

Hm, ich verstehe nicht, wie das einen Unterschied machen soll. Wenn Sie irgendwo tief in Wasm warten, müssen Sie den gesamten Call-Stack von mindestens dem Host-Eintrag bis zu diesem Punkt erfassen. Und Sie können diese Suspension (dh dieses Stapelsegment) so lange am Leben erhalten, wie Sie möchten, während Sie andere Anrufe von oben tätigen oder weitere Suspensionen erstellen. Und Sie können von woanders weitermachen (glaube ich?). Erfordert das nicht die ganze Implementierungsmaschinerie von abgegrenzten Fortsetzungen? Nur dass der Prompt bei der Wasm-Eingabe statt durch ein separates Konstrukt gesetzt wird.

@rossberg

Das mag auf einigen VMs zutreffen, ja. Wenn await und Coroutinen genau die gleiche VM-Arbeit benötigen, ist zumindest keine zusätzliche Arbeit erforderlich. In diesem Fall wäre der Vorteil des Await-Vorschlags die bequeme JS-Integration.

Ich denke, Sie können eine bequeme JS-Integration ohne vollständige Programmtransformation erhalten, wenn Sie nicht zulassen, dass das Modul erneut eingegeben wird.

Ich denke, Sie können eine bequeme JS-Integration ohne vollständige Programmtransformation erhalten, wenn Sie nicht zulassen, dass das Modul erneut eingegeben wird.

Dies klingt nach einem einfacheren Weg, um dies zu erreichen, aber dazu müssten alle besuchten Module in der Aufrufliste (oder als erster Schritt alle WebAssembly-Module) blockiert werden.

Dies klingt nach einem einfacheren Weg, um dies zu erreichen, aber dazu müssten alle besuchten Module in der Aufrufliste (oder als erster Schritt alle WebAssembly-Module) blockiert werden.

Richtig, genau wie atomic.wait .

@taralx

Ich denke, Sie können eine bequeme JS-Integration ohne vollständige Programmtransformation erhalten, wenn Sie nicht zulassen, dass das Modul erneut eingegeben wird.

Einerseits kann Re-entry sinnvoll sein, zum Beispiel lädt eine Spiel-Engine eine Datei herunter und möchte dabei nicht, dass die UI komplett angehalten wird (Asyncify erlaubt das heute). Aber auf der anderen Seite könnte der Wiedereintritt vielleicht verweigert werden, aber eine Anwendung könnte dafür mehrere Instanzen desselben Moduls erstellen (alle importieren denselben Speicher, veränderliche Globals usw.?), sodass ein Wiedereintritt ein Aufruf wäre zu einer anderen Instanz. Ich denke, wir können das in Toolchains zum Laufen bringen (es gäbe eine effektive Grenze für die Anzahl der gleichzeitig aktiven Wiedereintritte - gleich der Anzahl der Instanzen - was in Ordnung zu sein scheint).

Wenn Ihre Vereinfachung also VMs helfen würde, ist es definitiv eine Überlegung wert!

(Beachten Sie jedoch, dass wir, wie bereits erwähnt, meiner Meinung nach hier keine vollständige Programmumwandlung mit einer der besprochenen Optionen benötigen. Sie benötigen dies nur, wenn Sie sich in der schlechten Situation befinden, in der sich Asyncify befindet, in der es alles ist, was Sie tun können Toolchain-Ebene. Warten Sie ab, im schlimmsten Fall können Sie, wie mit @rossberg besprochen, das tun, was der Coroutines-Vorschlag intern tun würde. Aber Ihre Idee ist möglicherweise sehr interessant, wenn sie die Dinge einfacher macht!)

Einerseits kann Re-entry sinnvoll sein, zum Beispiel lädt eine Spiel-Engine eine Datei herunter und möchte dabei nicht, dass die UI komplett angehalten wird (Asyncify erlaubt das heute).

Ich bin mir aber nicht sicher, ob das eine Soundfunktion ist. Es scheint mir, dass dies jedoch _unerwartete Parallelität_ in die Anwendung einführen würde. Eine native Anwendung, die während des Renderns Assets lädt, würde intern zwei Threads verwenden, und jeder Thread würde einem WebWorker + SharedArrayBuffer zugeordnet werden. Wenn eine Anwendung Threads verwendet, könnte sie auch synchrone Web-Primitive von WebWorkers verwenden (wie sie zumindest in einigen Fällen zulässig sind). Andernfalls ist es immer möglich, asynchrone Operationen im Haupt-Thread blockierenden Operationen in einem Worker zuzuordnen, indem beispielsweise Atomics.wait verwendet wird.

Ich frage mich, ob der gesamte Anwendungsfall nicht bereits durch Multithreading im Allgemeinen gelöst wird. Durch die Verwendung von blockierenden Primitives in einem Worker bleibt der gesamte Stack (JS/Wasm/Browser-nativ) erhalten, was viel einfacher und robuster zu sein scheint.

Durch die Verwendung von blockierenden Primitives in einem Worker bleibt der gesamte Stack (JS/Wasm/Browser-nativ) erhalten, was viel einfacher und robuster zu sein scheint.

Das ist eigentlich eine weitere alternative Implementierung des eigenständigen Asyncify JS-Wrappers, mit dem ich experimentiert habe, aber obwohl es das Problem der Codegröße löst, war der Leistungsaufwand sogar viel höher als beim aktuellen Asyncify, das die Wasm-Transformation verwendet.

@alexp-sssup

Es scheint mir, dass dies jedoch zu einer unerwarteten Parallelität in der Anwendung führen würde.

Auf jeden Fall, ja - es muss sehr sorgfältig gemacht werden und kann Dinge kaputt machen. Wir haben damit gemischte Erfahrungen mit Asyncify gemacht, gute und schlechte (für ein gültiges Beispiel für einen Anwendungsfall: Eine Datei wird in JS heruntergeladen, und JS ruft wasm auf, um Speicherplatz freizugeben, in den sie kopiert werden kann, bevor es fortgesetzt wird). Aber in jedem Fall ist die Wiedereinreise kein entscheidender Teil dieses Vorschlags, so oder so.

Um das zu ergänzen, was @RReverser gesagt hat, ist ein weiteres Problem mit Threads, dass die Unterstützung für sie nicht universell ist und nicht sein wird . Aber Warten könnte überall sein.

In anderen Sprachen, in denen async/await eingeführt wurde, ist der Wiedereintritt absolut entscheidend. Es ist irgendwie der springende Punkt, dass andere Ereignisse passieren können, während man (a) wartet. Es scheint mir, dass der Wiedereintritt ziemlich wichtig ist.

Ist es außerdem nicht wahr, dass jedes Mal, wenn ein Modul eine externe Funktion aufruft, davon ausgehen muss, dass sie über einen seiner Exporte erneut eingegeben werden könnte (im obigen Beispiel sogar ohne Warten auf jeden Aufruf von und external Funktion ist kostenlos (kein Wortspiel beabsichtigt) um malloc aufzurufen).

Eine Anwendung könnte dafür mehrere Instanzen desselben Moduls erstellen (die alle denselben Speicher, veränderliche Globals usw. importieren?), sodass ein Wiedereintritt ein Aufruf einer anderen Instanz wäre

Nur für die gemeinsamen Speicher des Moduls. Die anderen Speicher müssen erneut instanziiert werden, was wichtig ist, um zu vermeiden, dass eine Operation während des Flugs auf eine andere Operation stampft.

Ich stelle fest, dass die nicht-reentrante Version davon bei jeder Einbettung mit Thread-Unterstützung polyfillable ist, falls jemand damit spielen und sehen wollte, wie nützlich es ist.

Ich stelle fest, dass die nicht-reentrante Version davon bei jeder Einbettung mit Thread-Unterstützung polyfillable ist, falls jemand damit spielen und sehen wollte, wie nützlich es ist.

Wie oben erwähnt, haben wir damit bereits gespielt, aber verworfen, da es eine noch schlechtere Leistung als die aktuelle Lösung bringt, nicht universell unterstützt wird und es außerdem sehr schwierig macht, WebAssembly.Global oder WebAssembly.Table zu teilen mit dem Hauptfaden ohne zusätzliche Hacks, was es zu einer schlechten Wahl für eine transparente Füllwatte macht.

Die aktuelle Lösung, die das Wasm-Modul umschreibt, leidet nicht unter diesen Problemen, sondern hat stattdessen erhebliche Kosten für die Dateigröße.

Daher eignet sich keines davon hervorragend für große reale Apps, was uns motiviert, uns mit nativer Unterstützung für asynchrone Integration wie hier beschrieben zu befassen.

schlechtere Leistung

Hast du eine Art Benchmark?

Ja, ich kann es teilen, wenn ich am Dienstag (oder wahrscheinlicher Mittwoch) wieder bei der Arbeit bin, oder es ist ziemlich einfach, eine zu zaubern, die nur selbst eine leere asynchrone JS-Funktion aufruft.

Danke. Ich könnte einen Mikrobenchmark erstellen, aber das wäre nicht sehr lehrreich.

Oh ja, meiner ist auch ein Mikrobenchmark, da wir nur am Overhead-Vergleich interessiert waren.

Das Problem bei einem Mikrobenchmark ist, dass wir nicht wissen, wie viel Latenz für eine echte Anwendung akzeptabel ist. Wenn es zusätzlich 1 ms dauert, ist das wirklich ein Problem, wenn die Anwendung beispielsweise nur Warteoperationen mit einer Rate von 1/s durchführt?

Ich denke, der Fokus auf die Geschwindigkeit eines atombasierten Ansatzes kann eine Ablenkung sein. Wie bereits erwähnt, funktioniert und wird Atomic nicht überall funktionieren (aufgrund von COOP/COEP) und auch nur ein Worker könnte den Atomic-Ansatz verwenden, da der Haupt-Thread nicht blockieren kann. Es ist eine nette Idee, aber für eine universelle Lösung brauchen wir so etwas wie Await.

Ich schlage es nicht als langfristige Lösung vor. Ich schlage vor, dass eine Polyfill, die sie verwendet, verwendet werden könnte, um zu sehen, ob eine nicht wiedereintrittsfähige Lösung für Menschen funktioniert.

@taralx Oh, ok, jetzt verstehe ich, danke.

@taralx :

Ich denke, Sie können eine bequeme JS-Integration ohne vollständige Programmtransformation erhalten, wenn Sie nicht zulassen, dass das Modul erneut eingegeben wird.

Das wäre schlecht. Dies bedeutet, dass das Zusammenführen mehrerer Module ihr Verhalten beeinträchtigen könnte. Das wäre die Antithese zur Modularität.

Als allgemeines Entwurfsprinzip sollte das Betriebsverhalten niemals von Modulgrenzen abhängig sein (abgesehen vom einfachen Scoping). Module sind lediglich ein Gruppierungs- und Scoping-Mechanismus in Wasm, und Sie möchten die Möglichkeit beibehalten, Dinge neu zu gruppieren (Module zu verknüpfen/zusammenzuführen/aufzuteilen), ohne dass das Verhalten eines Programms geändert wird.

@rossberg : Dies ist verallgemeinerbar, da es den Zugriff auf jedes Wasm-Modul blockiert, wie zuvor vorgeschlagen. Aber dann ist es wahrscheinlich zu einschränkend.

Das wäre schlecht. Dies bedeutet, dass das Zusammenführen mehrerer Module ihr Verhalten beeinträchtigen könnte. Das wäre die Antithese zur Modularität.

Das war mein Argument mit dem Polyfilling-Argument - atomic.wait bricht die Modularität nicht, also sollte dies auch nicht der Fall sein.

@taralx , atomic.wait verweist auf einen bestimmten Ort in einem bestimmten Speicher. Welchen Speicher und Ort würde await Blockieren verwenden, und wie würde man steuern, welche Module diesen Speicher gemeinsam nutzen?

@rossberg kannst du ein Szenario erläutern, von dem du denkst, dass es kaputt geht? Ich vermute, wir haben unterschiedliche Vorstellungen davon, wie die nicht-reentrante Version funktionieren würde.

@taralx , erwägen Sie, zwei Module A und B zu laden, die jeweils eine Exportfunktion bereitstellen, sagen wir A.f und B.g . Beide können await ausführen, wenn sie aufgerufen werden. Zwei Teilen des Clientcodes wird jeweils eine dieser Funktionen übergeben, und sie rufen sie unabhängig voneinander auf. Sie stören oder blockieren sich nicht gegenseitig. Dann führt jemand A und B in C zusammen oder refaktorisiert sie, ohne etwas am Code zu ändern. Plötzlich könnten sich beide Teile des Client-Codes unerwartet gegenseitig blockieren. Gruselige Fernwirkung durch versteckten Shared State.

Das macht Sinn. Aber das Zulassen des Wiedereintritts riskiert die Gleichzeitigkeit in Modulen, die es nicht erwarten, also ist es so oder so eine gespenstische Aktion aus der Ferne.

Aber Module sind doch schon wiedereinstiegsfähig, oder? Immer wenn ein Modul einen Import aufruft, kann der externe Code erneut in das Modul eintreten, was den globalen Zustand vor der Rückkehr ändern könnte. Ich kann nicht sehen, wie der Wiedereintritt während des vorgeschlagenen Wartens gespenstischer oder gleichzeitiger ist als das Aufrufen einer importierten Funktion. Vielleicht übersehe ich etwas?

(bearbeitet)

Hm, ja. Okay, eine importierte Funktion könnte also wieder in das Modul eintreten. Darüber muss ich mir eindeutig Gedanken machen.

Wenn Code ausgeführt wird und eine Funktion aufruft, gibt es zwei Möglichkeiten: Er weiß, dass die Funktion keine zufälligen Dinge aufruft, oder dass die Funktion zufällige Dinge aufruft. Im letzteren Fall ist ein Wiedereinstieg jederzeit möglich. Die gleichen Regeln gelten für await .

(habe meinen Kommentar oben editiert)

Danke an alle für die Diskussion bisher!

Zusammenfassend klingt es so, als gäbe es hier allgemeines Interesse, aber es gibt große offene Fragen, wie ob dies zu 100% auf der JS-Seite sein sollte oder nur zu 99% - klingt, als würde Ersteres die großen Sorgen einiger Leute beseitigen, und das würde für den Web-Fall in Ordnung sein, also ist das wahrscheinlich ok. Eine weitere große offene Frage ist, wie machbar dies in VMs wäre, über die wir weitere Informationen benötigen.

Ich werde einen Tagesordnungspunkt für das nächste CG-Treffen in 2 Wochen vorschlagen, um diesen Vorschlag zu diskutieren und für Stufe 1 zu prüfen, was bedeuten würde, ein Repo zu eröffnen und dort die offenen Fragen in separaten Themen ausführlicher zu diskutieren. (Ich glaube, das ist der richtige Prozess, aber bitte korrigieren Sie mich, wenn ich falsch liege.)

Nur für FYI
Wir werden einen Full-Stack-Switching-Vorschlag in ähnlicher Weise zusammenstellen
Zeitrahmen. Ich habe das Gefühl, dass dies Ihre Sonderfallvariante in Frage stellen könnte -
was denkst du?
Francis

Am Do, 28. Mai 2020 um 15:51 Uhr schrieb Alon Zakai [email protected] :

Danke an alle für die Diskussion bisher!

Zusammenfassend klingt es so, als gäbe es hier allgemeines Interesse, aber es gibt es
große offene Fragen wie, ob dies zu 100% auf der JS-Seite sein soll oder nur
99 % – klingt, als würde ersteres die großen Sorgen einiger Leute beseitigen
haben, und das wäre für den Web-Fall in Ordnung, also ist das wahrscheinlich ok.
Eine weitere große offene Frage ist, wie machbar dies in VMs wäre
Wir brauchen mehr Informationen über.

Ich werde einen Tagesordnungspunkt für das nächste CG-Meeting in 2 Wochen zur Diskussion vorschlagen
diesen Vorschlag und erwägen Sie ihn für Stufe 1, was die Eröffnung eines Repos bedeuten würde
und dort die offenen Fragen in gesonderten Ausgaben ausführlicher zu erörtern.
(Ich glaube, das ist der richtige Prozess, aber bitte korrigieren Sie mich, wenn ich falsch liege.)


Sie erhalten dies, weil Sie diesen Thread abonniert haben.
Antworten Sie direkt auf diese E-Mail und zeigen Sie sie auf GitHub an
https://github.com/WebAssembly/design/issues/1345#issuecomment-635649331 ,
oder abbestellen
https://github.com/notifications/unsubscribe-auth/AAQAXUCLZ4CJVQYEUBK23BLRT3TFLANCNFSM4NEJW2PQ
.

>

Franz McCabe
SW

@fgmccabe

Das sollten wir auf jeden Fall besprechen.

Im Allgemeinen vermute ich jedoch, dass dies, sofern sich Ihr Vorschlag nicht auf die JS-Seite konzentriert, dies nicht strittig machen würde (was zu 99% -100% auf der JS-Seite liegt).

Nachdem die Diskussion über die Implementierungsdetails nun abgeschlossen ist, möchte ich eine übergeordnete Besorgnis erneut äußern, die ich zuvor geäußert, aber fallen gelassen habe, um eine Diskussion nach der anderen zu führen.

Ein Programm besteht aus vielen Komponenten. Aus softwaretechnischer Sicht ist es wichtig, dass das Aufteilen von Komponenten in Teile oder das Zusammenführen von Komponenten das Verhalten des Programms nicht wesentlich ändert. Dies ist der Grund für das Modul-Kompositions-Prinzip, das beim letzten persönlichen CG-Meeting diskutiert wurde, und es ist implizit im Design vieler Sprachen enthalten.

Im Falle von Webprogrammen können diese verschiedenen Komponenten jetzt mit WebAssembly sogar in verschiedenen Sprachen geschrieben sein: JS oder Wasm. Tatsächlich könnten viele Komponenten genauso gut in beiden Sprachen geschrieben werden; Ich werde diese als "ambivalente" Komponenten bezeichnen. Im Moment sind die meisten ambivalenten Komponenten in JS geschrieben, aber ich denke, wir alle hoffen, dass immer mehr davon in Wasm umgeschrieben werden. Um diese "Code-Migration" zu erleichtern, sollten wir versuchen sicherzustellen, dass das Umschreiben einer Komponente auf diese Weise nicht ändert, wie sie mit der Umgebung interagiert. Als Spielzeugbeispiel: Ob eine bestimmte „apply“-Programmkomponente (f, x) => f(x) in JS oder in wasm geschrieben ist, sollte das Verhalten des Gesamtprogramms nicht beeinflussen. Dies ist ein Codemigrationsprinzip.

Leider scheinen alle Varianten dieses Vorschlags entweder gegen das Modul-Kompositionsprogramm oder das Code-Migrationsprinzip zu verstoßen. Ersteres wird verletzt, wenn await den Stack bis zu der Stelle erfasst, an der das aktuelle wasm-Modul zuletzt eingegeben wurde, da sich diese Grenze ändert, wenn Module getrennt oder kombiniert werden. Letzteres wird verletzt, wenn await den Stapel bis zu der Stelle erfasst, an der wasm zuletzt eingegeben wurde, da sich diese Grenze ändert, wenn Code von JS zu wasm migriert wird (so dass die Migration von etwas so Einfachem wie (f, x) => f(x) from JS to wasm kann das Verhalten des Gesamtprogramms erheblich verändern).

Ich glaube nicht, dass diese Verstöße auf schlechte Designentscheidungen dieses Vorschlags zurückzuführen sind. Das Problem scheint vielmehr darin zu bestehen, dass dieser Vorschlag zu vermeiden versucht, JS indirekt mächtiger zu machen, und dieses Ziel darin besteht, es zu zwingen, künstliche Grenzen zu setzen, die gegen diese Prinzipien verstoßen. Ich verstehe dieses Ziel vollkommen, aber ich vermute, dass dieses Problem immer häufiger auftreten wird: Das Hinzufügen von Funktionen zu WebAssembly auf eine Weise, die diese Prinzipien respektiert, erfordert oft das indirekte Hinzufügen von Funktionen zu JS, da JS die Einbettungssprache ist. Am liebsten würde ich dieses Problem direkt angehen (von dem ich wirklich keine Ahnung habe, wie ich es lösen soll). Wenn dies nicht der Fall ist, würde ich diese Änderung ausschließlich in der JS-API vornehmen, da hier JS der einschränkende Faktor ist, anstatt WebAssembly Anweisungen hinzuzufügen, für die wasm keine Interpretation hat.

Ich glaube nicht, dass diese Verstöße auf schlechte Designentscheidungen dieses Vorschlags zurückzuführen sind. Das Problem scheint vielmehr zu sein, dass dieser Vorschlag zu vermeiden versucht, JS indirekt mächtiger zu machen

Das ist wichtig, aber es ist nicht der Hauptgrund für das Design hier.

Der Hauptgrund für dieses Design ist, dass, obwohl ich voll und ganz zustimme, dass das Kompositionsprinzip für wasm sinnvoll ist, das grundlegende Problem, das wir im Web haben, darin besteht, dass JS und wasm in der Praxis nicht gleichwertig sind. Wir haben handgeschriebenes JS, das asynchron ist, und portiertes wasm, das synchronisiert ist. Mit anderen Worten, die Grenze zwischen ihnen ist eigentlich genau das Problem, das wir anzugehen versuchen. Insgesamt bin ich mir nicht sicher, ob ich damit einverstanden bin, dass das Kompositionsprinzip auf Wasm und JS angewendet werden sollte (aber vielleicht sollte es, könnte eine interessante Debatte sein).

Ich hatte gehofft, hier öffentlich mehr Diskussionen zu führen, aber um Zeit zu sparen, habe ich mich direkt an einige VM-Implementierer gewandt, da sich bisher nur wenige hier engagiert haben. Angesichts ihres Feedbacks zusammen mit der Diskussion hier denke ich leider, dass wir diesen Vorschlag pausieren sollten.

Await hat ein viel einfacheres beobachtbares Verhalten als allgemeine Coroutinen oder Stapelwechsel, aber die VM-Leute, mit denen ich gesprochen habe, stimmen @rossberg zu, dass die VM-Arbeit am Ende wahrscheinlich für beide ähnlich sein würde. Und zumindest einige VM-Leute glauben, dass wir sowieso Coroutinen oder Stack-Switching bekommen werden und dass wir damit die Anwendungsfälle von await unterstützen können. Das bedeutet, dass bei jedem Aufruf in wasm eine neue Coroutine/ein neuer Stack erstellt wird (anders als bei diesem Vorschlag), aber zumindest einige VM-Leute denken, dass dies schnell genug gemacht werden könnte.

Zusätzlich zu dem mangelnden Interesse von VM-Leuten hatten wir einige starke Einwände gegen diesen Vorschlag hier von @fgmccabe und @RossTate , wie oben besprochen. Wir sind uns in einigen Dingen nicht einig, aber ich schätze diese Standpunkte und die Zeit, die aufgewendet wurde, um sie zu erklären.

Zusammenfassend fühlt es sich insgesamt so an, als wäre es Zeitverschwendung, zu versuchen, hier voranzukommen. Aber danke an alle, die sich an der Diskussion beteiligt haben! Und hoffentlich motiviert dies zumindest zur Priorisierung von Coroutinen / Stapelwechseln.

Beachten Sie, dass der JS-Teil dieses Vorschlags in Zukunft relevant sein kann, da JS-Zucker grundsätzlich für eine bequeme Promise-Integration dient. Wir müssen auf Stapelwechsel oder Koroutinen warten und sehen, ob dies darüber hinaus funktionieren könnte. Aber ich glaube nicht, dass es sich lohnt, das Thema dafür offen zu halten, also schließe ich es.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen

Verwandte Themen

chicoxyzzy picture chicoxyzzy  ·  5Kommentare

jfbastien picture jfbastien  ·  6Kommentare

konsoletyper picture konsoletyper  ·  6Kommentare

beriberikix picture beriberikix  ·  7Kommentare

cretz picture cretz  ·  5Kommentare