Design: Propuesta: En espera

Creado en 18 may. 2020  ·  96Comentarios  ·  Fuente: WebAssembly/design

@rreverser y me gustaría proponer una nueva propuesta para WebAssembly: Await .

La motivación de la propuesta es ayudar al código " sincrónico " compilado a WebAssembly, que hace algo como una lectura de un archivo:

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

Este código no se puede implementar fácilmente en un entorno de host que es principalmente asíncrono y que implementaría "leer desde un archivo" de forma asíncrona, por ejemplo en la Web,

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

En otras palabras, el objetivo es ayudar con el problema de sincronización/asincronización que es tan común con wasm en la Web.

El problema de sincronización/asincronización es un problema grave. Si bien se puede escribir código nuevo teniendo en cuenta este código, las bases de código existentes de gran tamaño a menudo no se pueden refactorizar para evitarlo, lo que significa que no se pueden ejecutar en la Web. Tenemos Asyncify que instrumenta un archivo wasm para permitir la pausa y la reanudación, y que ha permitido que se transfieran algunas bases de código, por lo que no estamos completamente bloqueados aquí. Sin embargo, instrumentar el wasm tiene una sobrecarga significativa, algo así como un aumento del 50 % en el tamaño del código y una ralentización del 50 % en promedio (pero a veces mucho peor), porque agregamos instrucciones para escribir/leer en el estado local y llamar a la pila y así sucesivamente. ¡Esa sobrecarga es una gran limitación y descarta Asyncify en muchos casos!

El objetivo de esta propuesta es permitir pausar y reanudar la ejecución de manera eficiente (en particular, sin sobrecarga como la que tiene Asyncify) para que todas las aplicaciones que encuentren el problema de sincronización/asincronía puedan evitarlo fácilmente. Personalmente, tenemos la intención de esto principalmente para la Web, donde puede ayudar a que WebAssembly se integre mejor con las API web, pero los casos de uso fuera de la Web también pueden ser relevantes.

La idea en breve

El problema central aquí es entre el código wasm que es síncrono y el entorno del host que es asíncrono. Por lo tanto, nuestro enfoque se centra en el límite de una instancia de wasm y el exterior. Conceptualmente, cuando se ejecuta una nueva instrucción await , la instancia de wasm "espera" algo del exterior. Lo que significa "esperar" diferirá en diferentes plataformas y puede no ser relevante en todas las plataformas (como no todas las plataformas pueden encontrar relevante la propuesta de wasm atomics), pero en la plataforma web específicamente, la instancia de wasm esperaría en una Promesa y pausaría hasta que se resuelva o rechace. Por ejemplo, una instancia de wasm podría hacer una pausa en una operación de red fetch y escribirse algo así en .wat :

;; 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

Tenga en cuenta la similitud general con await en JS y otros idiomas. Si bien esto no es idéntico a ellos (consulte los detalles a continuación), el beneficio clave es que permite escribir código de apariencia síncrona (o más bien, compilar código de apariencia síncrona en wasm).

Los detalles

Especificaciones de wasm de núcleo

Los cambios en la especificación central de wasm son mínimos:

  • Agregue un tipo waitref .
  • Agregue una instrucción await .

Se especifica un tipo para cada instrucción await (como call_indirect ), por ejemplo:

;; 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

El tipo debe recibir waitref y puede devolver cualquier tipo (o nada).

await solo se define en términos de hacer que el entorno host haga algo. Es similar en ese sentido a la instrucción unreachable , que en la Web hace que el host arroje un RuntimeError , pero eso no está en la especificación central. Del mismo modo, la especificación central de wasm solo dice que await está destinado a esperar algo del entorno del host, pero no cómo lo haríamos realmente, lo que podría ser muy diferente en diferentes entornos del host.

¡Eso es todo para la especificación central de wasm!

Especificaciones Wasm JS

Los cambios en la especificación wasm JS (que afectan solo a entornos JS como la Web) son más interesantes:

  • Un valor válido waitref es una Promesa JS.
  • Cuando se ejecuta await en una Promesa, toda la instancia de wasm se detiene y espera a que la Promesa se resuelva o rechace.
  • Si la Promesa se resuelve, la instancia reanuda la ejecución después de enviar a la pila el valor recibido de la Promesa (si lo hay)
  • Si la Promesa se rechaza, reanudamos la ejecución y lanzamos una excepción wasm desde la ubicación de await .

Por "toda la instancia de wasm se detiene" queremos decir que se conserva todo el estado local (la pila de llamadas, los valores locales, etc.) para que podamos reanudar la ejecución actual más tarde, como si nunca hubiéramos hecho una pausa (por supuesto, el estado global puede haber cambiado, como se pudo haber escrito la Memoria). Mientras esperamos, el bucle de eventos JS funciona normalmente y pueden suceder otras cosas. Cuando reanudamos más tarde (si no rechazamos la Promesa, en cuyo caso se lanzaría una excepción) continuamos exactamente donde lo dejamos, básicamente como si nunca nos detuviéramos (pero mientras tanto han sucedido otras cosas y el estado global puede haber cambiado). cambiado, etc).

¿Qué le parece a JS cuando llama a una instancia de wasm que luego se detiene? Para explicarlo, primero echemos un vistazo a un ejemplo común que se encuentra al migrar aplicaciones nativas a wasm, un bucle de eventos:

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

Imagine que esta función se llama una vez por requestAnimationFrame . Ejecuta las tareas que se le asignan, que pueden incluir: renderizado, física, audio y recuperación de red. Si tenemos un evento de obtención de red, entonces y solo entonces terminamos ejecutando una instrucción $# await fetch . Podemos hacer eso 0 veces para una llamada de event_loop_iteration , o 1 vez, o muchas veces. Solo sabemos si terminamos haciéndolo durante la ejecución de este wasm, no antes, y en particular no en la persona que llama JS de esta exportación de wasm. Entonces, la persona que llama debe estar lista para que la instancia se pause o no.

Una situación algo análoga puede ocurrir en JavaScript puro:

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

foo obtiene una función JS bar y la llama con algunos datos. En JS bar puede ser una función asíncrona o puede ser normal. Si es asíncrono, devuelve una promesa y solo finaliza la ejecución más tarde. Si es normal, se ejecuta antes de regresar y devuelve el resultado real. foo puede asumir que sabe qué tipo es bar (no hay ningún tipo escrito en JS, de hecho, bar puede que ni siquiera sea una función), o puede manejar ambos tipos de funciones sean totalmente generales.

¡Ahora, normalmente sabes exactamente qué conjunto de funciones podría ser bar ! Por ejemplo, puede haber escrito foo y los posibles bar en coordinación, o haber documentado exactamente cuáles son las expectativas. Pero la interacción wasm/JS de la que estamos hablando aquí es en realidad más similar al caso en el que no tiene un acoplamiento tan estrecho entre las cosas y donde, de hecho, necesita manejar ambos casos. Como se mencionó anteriormente, el ejemplo event_loop_iteration requiere eso. Pero incluso de manera más general, a menudo el wasm es su aplicación compilada mientras que el JS es un código genérico de "tiempo de ejecución", por lo que JS tiene que manejar todos los casos. JS puede hacerlo fácilmente, por supuesto, por ejemplo, usando result instanceof Promise para verificar el resultado, o use 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");
}

(tenga en cuenta que si no necesitamos ese console.log , entonces no necesitaríamos el JS await en este ejemplo, y solo tendríamos una llamada normal a una exportación de wasm)

Para resumir lo anterior, proponemos que el comportamiento de una instancia de wasm en pausa se modele en el caso JS de una función que puede o no ser asíncrona, que podemos establecer como:

  • Cuando se ejecuta un await , la instancia de wasm vuelve a salir inmediatamente a quien haya llamado (por lo general, sería JS llamando a una exportación de wasm, pero consulte las notas más adelante). La persona que llama recibe una Promesa que puede usar para saber cuándo concluye la ejecución del wasm y para obtener un resultado, si lo hay.

Compatibilidad con cadenas de herramientas/bibliotecas

En nuestra experiencia con Asyncify y herramientas relacionadas, es fácil (¡y divertido!) escribir un poco de JS para manejar una instancia de wasm en espera. Aparte de las opciones mencionadas anteriormente, una biblioteca podría hacer una de las siguientes:

  1. Envuelva una instancia de wasm para que sus exportaciones siempre devuelvan una Promesa. Eso brinda una interfaz simple y agradable al exterior (sin embargo, agrega sobrecarga a las llamadas rápidas a wasm que no se detienen). Esto es lo que hace la biblioteca auxiliar Asyncify independiente , por ejemplo.
  2. Escriba algún estado global cuando una instancia se detenga y compruébelo desde el JS que llamó a la instancia. Eso es lo que hace la integración Asyncify de Emscripten, por ejemplo.

Se puede construir mucho más sobre tales enfoques u otros. Preferimos dejar todo eso a las cadenas de herramientas y bibliotecas para evitar la complejidad en la propuesta y en las máquinas virtuales.

Implementación y Desempeño

Varios factores deberían ayudar a mantener simples las implementaciones de VM:

  1. Una pausa/reanudar ocurre solo en una espera, y conocemos sus ubicaciones estáticamente dentro de cada función.
  2. Cuando retomamos, continuamos exactamente desde donde dejamos las cosas, y solo lo hacemos una vez. En particular, nunca "bifurcamos" la ejecución: nada aquí regresa dos veces, a diferencia de setjmp de C o una corrutina en un sistema que permite la clonación/bifurcación.
  3. Es aceptable si la velocidad de un await es más lenta que una llamada normal a JS, ya que estaremos esperando una Promesa, que como mínimo implica que se asignó una Promesa y que esperamos en el bucle de eventos ( que tiene una sobrecarga mínima además de esperar potencialmente otras cosas que se están ejecutando actualmente ). Es decir, los casos de uso aquí no exigen que los implementadores de máquinas virtuales encuentren formas de hacer que await sea increíblemente rápido. Solo queremos que await sea eficiente en comparación con los requisitos aquí y, en particular, esperamos que sea mucho más rápido que la gran sobrecarga de Asyncify.

Dado lo anterior, una implementación natural es copiar la pila cuando hacemos una pausa. Si bien eso tiene algunos gastos generales, dadas las expectativas de rendimiento aquí, debería ser muy razonable. Y si copiamos la pila solo cuando hacemos una pausa, podemos evitar hacer un trabajo adicional para prepararnos para la pausa. Es decir, no debería haber una sobrecarga general adicional (¡que es muy diferente de Asyncify!)

Tenga en cuenta que si bien copiar la pila es un enfoque natural aquí, no es una operación completamente trivial, ya que la copia puede no ser un simple memcpy, dependiendo de las partes internas de la VM. Por ejemplo, si la pila contiene punteros a sí misma, entonces será necesario ajustarlos o que la pila sea reubicable. Alternativamente, podría ser posible volver a copiar la pila a su posición original antes de reanudarla, ya que, como se mencionó anteriormente, nunca se "bifurca".

Tenga en cuenta también que nada en esta propuesta requiere copiar la pila. Quizás algunas implementaciones puedan hacer otras cosas, gracias a los factores de simplificación mencionados en los 3 puntos anteriores de esta sección. El comportamiento observable aquí es bastante simple y el manejo explícito de la pila no forma parte de él.

¡Estamos muy interesados ​​en escuchar los comentarios de los implementadores de máquinas virtuales sobre esta sección!

aclaraciones

Esta propuesta solo detiene la ejecución de WebAssembly de vuelta al llamador de la instancia de wasm. No permite pausar marcos de pila de host (JS o navegador). await opera en una instancia de wasm y solo afecta a los marcos de pila dentro de ella.

Está bien llamar a la instancia de WebAssembly mientras se ha producido una pausa, y varios eventos de pausa/reanudación pueden estar en curso a la vez. (Tenga en cuenta que si la VM toma el enfoque de copiar la pila, esto no significa que se debe asignar una nueva pila cada vez que ingresamos al módulo, ya que solo necesitamos copiarlo si realmente hacemos una pausa).

Conexión con otras propuestas

Excepciones

Promesa de rechazo lanzando una excepción significa que esta propuesta depende de la propuesta de excepciones de wasm.

corrutinas

La propuesta de rutinas de Andreas Rossberg también se ocupa de pausar y reanudar la ejecución. Sin embargo, aunque existe cierta superposición conceptual, no creemos que las propuestas compitan. Ambos son útiles porque están enfocados en diferentes casos de uso. En particular, la propuesta de corrutinas permite que las corrutinas se cambien entre wasm interno , mientras que la propuesta de await permite que una instancia completa espere el entorno externo . Y la forma en que se hacen ambas cosas conduce a características diferentes.

Específicamente, la propuesta de rutinas maneja la creación de pilas de manera explícita (se proporcionan instrucciones para crear una rutina, pausar una, etc.). La propuesta de espera solo habla de pausar y reanudar y, por lo tanto, el manejo de la pila está implícito . El manejo de pila explícito es apropiado cuando sabe que está creando corrutinas específicas, mientras que el implícito es apropiado cuando solo sabe que necesita esperar algo durante la ejecución (vea el ejemplo anterior con event_loop_iteration ).

Las características de rendimiento de esos dos modelos pueden ser muy diferentes. Si, por ejemplo, creamos una corrutina cada vez que ejecutamos código que podría hacer una pausa (nuevamente, a menudo no lo sabemos de antemano), eso podría asignar memoria innecesariamente. El comportamiento observado de await es más simple que lo que pueden hacer las corrutinas generales y, por lo tanto, puede ser más simple de implementar.

Otra diferencia importante es que await es una sola instrucción que proporciona todo lo que necesita un módulo wasm para corregir la falta de coincidencia de sincronización/asincronía que wasm tiene con la Web (consulte el primer ejemplo de .wat del mismo comenzando). También es muy fácil de usar en el lado de JS, que solo puede proporcionar y/o recibir una Promesa (aunque puede ser útil agregar un pequeño código de biblioteca, como se mencionó anteriormente, puede ser muy mínimo).

En teoría, las dos propuestas podrían diseñarse para ser complementarias. ¿Quizás await podría ser una de las instrucciones en la propuesta de rutinas de alguna manera? Otra opción es permitir que un await opere en una rutina (básicamente, le da a una instancia de wasm una manera fácil de esperar los resultados de la rutina).

WASI#276

Por coincidencia , WASI #276 fue publicado por @tqchen justo cuando terminábamos de escribir esto. Estamos muy contentos de ver eso, ya que comparte nuestra creencia de que las corrutinas y el soporte asíncrono son funcionalidades separadas.

Creemos que una instrucción await podría ayudar a implementar algo muy similar a lo que se propone allí (opción C3), con la diferencia de que no sería necesario que hubiera llamadas al sistema asíncronas especiales, sino que algunas llamadas al sistema podrían devolver un waitref que luego puede ser await -ed.

Para JavaScript, definimos esperar como pausar una instancia de wasm, lo cual tiene sentido porque podemos tener varias instancias además de JavaScript en la página. Sin embargo, en algunos entornos de servidor puede haber solo el host y una sola instancia de wasm y, en ese caso, la espera puede ser mucho más simple, tal vez esperando literalmente en un descriptor de archivo o en la GPU. O esperar podría pausar toda la máquina virtual wasm pero seguir ejecutando un bucle de eventos. Nosotros mismos no tenemos ideas específicas aquí, pero según la discusión en ese número, puede haber posibilidades interesantes aquí, ¡tenemos curiosidad por lo que piensa la gente!

Caso de la esquina: instancia de wasm => instancia de wasm => espera

En un entorno JS, cuando una instancia de wasm se detiene, regresa inmediatamente a quien la llamó. Describimos lo que sucede si la persona que llama es de JS, y lo mismo sucede si la persona que llama es el navegador (por ejemplo, si hicimos un setTimeout en una exportación de wasm que se detiene; pero no sucede nada interesante allí, ya que la Promesa devuelta simplemente se ignora). Pero hay otro caso, en el que la llamada proviene de wasm, es decir, donde la instancia de wasm A llama directamente a una exportación desde la instancia B y B se detiene. La pausa nos hace salir inmediatamente de B y devolver Promise .

Cuando la persona que llama es JavaScript, como lenguaje dinámico, esto es un problema menor y, de hecho, es razonable esperar que la persona que llama verifique el tipo como se discutió anteriormente. Cuando la persona que llama es WebAssembly, que se escribe estáticamente, esto es incómodo. Si no hacemos algo en la propuesta para esto, el valor se convertirá, en nuestro ejemplo de una Promesa a cualquier instancia que A espera (si es un i32 , se convertiría en a 0 ). En su lugar, sugerimos que ocurra un error:

  • Si una instancia de wasm llama (directamente o usando call_indirect ) a una función de otra instancia de wasm, y mientras se ejecuta en la otra instancia se ejecuta await , entonces se genera una excepción RuntimeError lanzado desde la ubicación del await .

Es importante destacar que esto se puede hacer sin sobrecarga a menos que se haga una pausa, es decir, manteniendo las llamadas wasm instance -> wasm instance normales a toda velocidad, al verificar la pila solo cuando se hace una pausa.

Tenga en cuenta que los usuarios que desean algo como una instancia de wasm para llamar a otra y hacer que esta última haga una pausa pueden hacerlo, pero deben agregar algo de JS entre los dos.

Otra opción aquí es que una pausa se propague también al wasm que realiza la llamada, es decir, todos los wasm se detendrían hasta JS, lo que podría abarcar varias instancias de wasm. Esto tiene algunas ventajas, como que los límites del módulo wasm dejan de importar, pero también desventajas, como que la propagación es menos intuitiva (el autor de la instancia de llamada puede no esperar tal comportamiento) y que agregar JS en el medio podría cambiar el comportamiento (también potencialmente inesperadamente). Requerir que los usuarios tengan JS en el medio, como se mencionó anteriormente, parece menos riesgoso.

Otra opción podría ser que algunas exportaciones de wasm se marcaran como asíncronas mientras que otras no, y entonces podríamos saber estáticamente qué es qué y no permitir llamadas incorrectas; pero vea el ejemplo anterior de event_loop_iteration que es un caso común que no se resolvería marcando exportaciones, y también hay llamadas indirectas, por lo que no podemos evitar el problema de esa manera.

Enfoques alternativos considerados

¿Quizás no necesitamos una nueva instrucción await en absoluto, si wasm se detiene cada vez que una importación JS devuelve una Promesa? El problema es que ahora mismo cuando JS devuelve una Promesa que no es un error. Tal cambio incompatible con versiones anteriores significaría que wasm ya no puede recibir una Promesa sin hacer una pausa, pero eso también podría ser útil.

Otra opción que consideramos es marcar las importaciones de alguna manera para decir "esta importación debe pausarse si devuelve una Promesa". Pensamos en varias opciones sobre cómo marcarlos, ya sea en el lado JS o en el lado wasm, pero no encontramos nada que se sintiera bien. Por ejemplo, si marcamos las importaciones en el lado JS, el módulo wasm no sabría si una llamada a una importación se detiene o no hasta el paso del enlace, cuando llegan las importaciones. Es decir, las llamadas a importaciones y pausas se "mezclarían". Parece que lo más sencillo es tener una nueva instrucción para esto, await , que es explícita sobre la espera. En teoría, tal capacidad también puede ser útil fuera de la Web (consulte las notas anteriores), por lo que tener una instrucción para todos puede hacer que las cosas sean más consistentes en general.

Discusiones relacionadas anteriores

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

¡Gracias por leer, los comentarios son bienvenidos!

Comentario más útil

Esperaba tener más discusión públicamente aquí, pero para ahorrar tiempo me comuniqué directamente con algunos implementadores de VM, ya que pocos se han involucrado aquí hasta ahora. Dados sus comentarios junto con la discusión aquí, lamentablemente creo que deberíamos hacer una pausa en esta propuesta.

Await tiene un comportamiento observable mucho más simple que las corrutinas generales o el cambio de pila, pero la gente de VM con la que hablé está de acuerdo con @rossberg en que el trabajo de VM al final probablemente sería similar para ambos. Y al menos algunas personas de VM creen que obtendremos corrutinas o cambio de pila de todos modos, y que podemos respaldar los casos de uso de await usando eso. Eso significará crear una nueva corrutina/pila en cada llamada al wasm (a diferencia de esta propuesta), pero al menos algunas personas de VM piensan que podría hacerse lo suficientemente rápido.

Además de la falta de interés de la gente de VM, hemos tenido algunas objeciones fuertes a esta propuesta aquí por parte de @fgmccabe y @RossTate , como se discutió anteriormente. No estamos de acuerdo en algunas cosas, pero aprecio esos puntos de vista y el tiempo que se dedicó a explicarlos.

En conclusión, en general parece que sería una pérdida de tiempo para todos tratar de avanzar aquí. ¡Pero gracias a todos los que participaron en la discusión! Y con suerte, al menos esto motiva a priorizar el cambio de rutinas/pilas.

Tenga en cuenta que la parte JS de esta propuesta puede ser relevante en el futuro, ya que JS sugar básicamente para la integración conveniente de Promise. Tendremos que esperar el cambio de pila o las corrutinas y ver si esto podría funcionar además de eso. Pero no creo que valga la pena mantener el tema abierto por eso, así que cerrando.

Todos 96 comentarios

¡Excelente redacción! Me gusta la idea de la suspensión controlada por el anfitrión. La propuesta de @rossberg también analiza los sistemas de efectos funcionales, y admito que no soy un experto en ellos, pero a primera vista parece que podrían satisfacer la misma necesidad de flujo de control no local.

Con respecto a: "Dado lo anterior, una implementación natural es copiar la pila cuando hacemos una pausa". ¿Cómo funcionaría esto para la pila de ejecución? Me imagino que la mayoría de los motores JIT comparten la pila de ejecución nativa de C entre JS y wasm, por lo que no estoy seguro de qué significaría guardar y restaurar en este contexto. ¿Significa esta propuesta que la pila de ejecución de wasm debería virtualizarse de alguna manera? IIUC evitar el uso de la pila C de esta manera fue bastante complicado cuando python intentó hacer algo similar: https://github.com/stackless-dev/stackless/wiki.

Comparto una preocupación similar con @ sbc100. Copiar la pila es inherentemente una operación bastante difícil, especialmente si su VM aún no tiene una implementación de GC.

@sbc100

¿Significa esta propuesta que la pila de ejecución de wasm debería virtualizarse de alguna manera?

Tengo que dejar esto en manos de los implementadores de VM, ya que no soy un experto en eso. Y no entiendo la conexión con Python sin pila, pero tal vez no sé lo suficientemente bien como para entender la conexión, ¡lo siento!

Pero en general: varios enfoques coroutine funcionan manipulando el puntero de la pila en un nivel bajo . Esos enfoques pueden ser una opción aquí. Queríamos señalar que incluso si la pila tiene que copiarse como parte de dicho enfoque, hacerlo tiene una sobrecarga aceptable en este contexto.

(No estamos seguros de si esos enfoques pueden funcionar en máquinas virtuales wasm o no; esperamos saber de los implementadores si es así o no, y si hay mejores opciones).

@lachlansneff

¿Puede explicar con más detalle lo que quiere decir con GC haciendo las cosas más fáciles? no sigo

Los GC de @kripken a menudo (pero no siempre) tienen la capacidad de recorrer una pila, lo cual es necesario si necesita volver a escribir punteros en la pila para apuntar a la nueva pila. Quizás alguien que sepa más sobre JSC pueda confirmar o negar esto.

@lachlansneff

Gracias, ahora veo lo que dices.

No sugerimos que recorrer la pila de una manera tan completa (identificando cada local hasta arriba, etc.) sea necesario para hacer esto. (Para otros enfoques posibles, vea el enlace en mi último comentario sobre los métodos de implementación de rutinas de bajo nivel).

Pido disculpas por la terminología de "copiar la pila" en la propuesta. Veo que no fue lo suficientemente claro, según sus comentarios y los de @sbc100 . Nuevamente, no queremos sugerir un enfoque de implementación de VM específico. Solo queríamos decir que si es necesario copiar la pila en algún enfoque, eso no sería un problema para la velocidad.

En lugar de sugerir un enfoque de implementación específico, ¡esperamos escuchar de la gente de VM cómo creen que se podría hacer esto!

Estoy muy emocionado de ver esta propuesta. Lucet ha tenido operadores yield y resume desde hace un tiempo, y los usamos precisamente para interactuar con el código asíncrono que se ejecuta en el entorno de host de Rust.

Esto fue bastante sencillo de agregar a Lucet, ya que nuestro diseño ya se comprometió a mantener una pila separada para la ejecución de Wasm, pero me imagino que podría presentar algunas dificultades de implementación para las máquinas virtuales que no lo hacen.

¡Esta propuesta suena genial! Hemos estado tratando de encontrar una buena manera de administrar el código asíncrono en wasmer-js durante un tiempo (ya que no tenemos acceso a las partes internas de la VM en un contexto de navegador).

En lugar de sugerir un enfoque de implementación específico, ¡esperamos escuchar de la gente de VM cómo creen que se podría hacer esto!

Creo que quizás usar la estrategia de devolución de llamada para funciones asíncronas podría ser la forma más fácil de hacer que todo funcione y también de una manera independiente del idioma.

Parece que se puede llamar a .await en un JsPromise dentro de una función Rust usando wasm-bindgen-futures ? ¿Cómo puede funcionar esto sin la instrucción await propuesta aquí? Lo siento por mi ignorancia, estoy buscando soluciones para llamar a buscar dentro de wasm y estoy aprendiendo sobre Asyncify, pero parece que la solución de Rust es más simple. ¿Qué me estoy perdiendo aquí? ¿Alguien me lo puede aclarar?

Estoy muy entusiasmado con esta propuesta. La principal ventaja de la propuesta es su simplicidad, ya que podemos crear API que se sincronizan con el punto de vista de wasm y hace que sea mucho más fácil portar aplicaciones sin tener que pensar explícitamente en devoluciones de llamada y async/await. Nos permitiría llevar el aprendizaje automático basado en WASM y WebGPU a máquinas virtuales wasm nativas utilizando una única API nativa y ejecutarse tanto en la web como de forma nativa.

Una cosa que creo que vale la pena discutir es la firma de las funciones que potencialmente esperan llamadas. Imagina que tenemos la siguiente función

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

La firma de la función correspondiente es () => i32 . Según la nueva propuesta, las llamadas a prueba podrían devolver i32 o Promise<i32> . Tenga en cuenta que es más difícil pedirle al usuario que declare estáticamente una nueva firma (debido al costo de la transferencia de código, y podrían ser llamadas indirectas dentro de la función que no sabemos que esperan llamadas).

¿Deberíamos tener un modo de llamada separado en la función exportada (por ejemplo, llamada asíncrona) para indicar que se permite esperar durante el tiempo de ejecución?

En cuanto a la terminología, la operación propuesta es como una operación de rendimiento en los sistemas operativos. Dado que cede el control al sistema operativo (en este caso, la máquina virtual wasm) para esperar a que finalice la llamada al sistema.

Si entiendo esta propuesta correctamente, creo que es más o menos equivalente a eliminar la restricción de que await en JS solo se puede usar en funciones de async . Es decir, en el lado wasm waitref podría ser externref y en lugar de una instrucción await podría tener una función importada $await : [externref] -> [] , y en el lado JS podría proporcionar foo(promise) => await promise como la función para importar. En la otra dirección, si fuera un código JS que quisiera await en una promesa fuera de la función async , podría proporcionar esa promesa a un módulo wasm que simplemente llame a await en la entrada ¿Es ese un entendimiento correcto?

@RossTate No del todo, AIUI. El código wasm puede await una promesa (llámelo promise1 ), pero solo producirá la ejecución wasm, no el JS. El código wasm devolverá una promesa diferente (llámela promise2 ) a la persona que llama JS. Cuando promise1 se resuelve, la ejecución de wasm continúa. Finalmente, cuando ese código wasm sale normalmente, promise2 se resolverá con el resultado de la función wasm.

@tqchen

¿Deberíamos tener un modo de llamada separado en la función exportada (por ejemplo, llamada asíncrona) para indicar que se permite esperar durante el tiempo de ejecución?

Interesante, ¿dónde ves el beneficio? Como dijiste, realmente no hay forma de estar seguro de si una exportación terminará haciendo un await o no, en situaciones de portabilidad comunes, por lo que, en el mejor de los casos, solo podría usarse algunas veces. ¿Ayudaría esto a las máquinas virtuales internamente, aunque tal vez?

Tener una declaración explícita podría garantizar que el usuario indique claramente su intención, y la VM podría generar un mensaje de error adecuado si la intención del usuario no es realizar una llamada que se ejecute de forma asíncrona.

Desde el punto de vista del usuario también hace que la escritura del código sea más consistente. Por ejemplo, el usuario podría escribir el siguiente código, incluso si la prueba no llama a await, y la interfaz del sistema devuelve Promise.resolve(test()) automáticamente.

await inst.exports_async.test();

Desde el punto de vista del usuario también hace que la escritura del código sea más consistente. Por ejemplo, el usuario podría escribir el siguiente código, incluso si la prueba no llama a await, y la interfaz del sistema devuelve Promise.resolve(test()) automáticamente.

@tqchen Tenga en cuenta que el usuario ya puede hacer esto como se muestra en el ejemplo de la prueba de propuesta. Es decir, JavaScript ya admite y maneja valores sincrónicos y asincrónicos en un operador await de la misma manera.

Si la sugerencia es hacer cumplir un solo tipo estático, entonces creemos que esto se puede hacer en el nivel de sistema de tipo o pelusa o en un nivel de envoltorio de JavaScript sin introducir complejidad en el lado central de WebAssembly o restringir a los implementadores de tales envoltorios.

Ah, gracias por la corrección, @binji.

En ese caso, ¿es lo siguiente más o menos equivalente? Agregue una función WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") a la API de JS. Supongamos que moduleBytes tiene una cantidad de importaciones más una importación adicional import "name1" "name2" (func (param externref)) . Luego, esta función instancia las importaciones con los valores dados por imports e instancia la importación adicional con lo que es conceptualmente await . Cuando se crean funciones exportadas a partir de este módulo, se protegen de modo que cuando se llama a este await sube por la pila para encontrar el primer protector y luego copia el contenido de la pila en una nueva Promesa que luego se inmediatamente devuelto.

Funcionaría eso? Mi sensación es que esta propuesta se puede realizar únicamente modificando la API de JS sin necesidad de modificar WebAssembly. Por supuesto, incluso entonces todavía agrega muchas funciones útiles.

@kripken ¿Cómo se manejaría la función start ? ¿Rechazaría estáticamente await , o de alguna manera interactuaría con la creación de instancias de Wasm?

@malbarbo wasm-bindgen-futures te permite ejecutar código async en Rust. Eso significa que debe escribir su programa de forma asíncrona: debe marcar sus funciones como async y debe usar .await . Pero esta propuesta le permite ejecutar código asíncrono sin usar async o .await , sino que parece una llamada de función síncrona normal.

En otras palabras, actualmente no puede usar API de SO sincrónicas (como std::fs ) porque la web solo tiene API asíncronas. Pero con esta propuesta, podría usar API de sistema operativo síncronas: internamente usarían Promises, pero parecerían síncronas con Rust.

Incluso si se implementa esta propuesta, wasm-bindgen-futures seguirá existiendo y seguirá siendo útil, porque está manejando un caso de uso diferente (ejecutando funciones async ). Y las funciones async son útiles porque se pueden paralelizar fácilmente.

@RossTate Parece que su sugerencia es bastante similar a la cubierta en "Enfoques alternativos considerados":

Otra opción que consideramos es marcar las importaciones de alguna manera para decir "esta importación debe pausarse si devuelve una Promesa". Pensamos en varias opciones sobre cómo marcarlos, ya sea en el lado JS o en el lado wasm, pero no encontramos nada que se sintiera bien. Por ejemplo, si marcamos las importaciones en el lado JS, el módulo wasm no sabría si una llamada a una importación se detiene o no hasta el paso del enlace, cuando llegan las importaciones. Es decir, las llamadas a importaciones y pausas se "mezclarían". Parece que lo más sencillo es simplemente tener una nueva instrucción para esto, await, que es explícita sobre esperar. En teoría, tal capacidad también puede ser útil fuera de la Web (consulte las notas anteriores), por lo que tener una instrucción para todos puede hacer que las cosas sean más consistentes en general.

¿Cómo se manejaría la función de inicio? ¿Rechazaría estáticamente la espera o interactuaría de alguna manera con la creación de instancias de Wasm?

@Pauan No cubrimos esto específicamente, pero creo que no hay nada que nos impida permitir await en start también. En este caso, la Promesa devuelta por instantiate{Streaming} aún se resolvería/rechazaría de forma natural cuando la función de inicio haya terminado de ejecutarse por completo, con la única diferencia de que esperaría por las promesas de await .

Dicho esto, se aplican las mismas limitaciones que hoy y, por ahora, no sería demasiado útil para los casos que requieren acceso, por ejemplo, a la memoria exportada.

@RReverser ¿Cómo funcionaría eso para el new WebAssembly.Instance síncrono (que se usa en los trabajadores)?

Interesante punto @Pauan sobre el inicio!

Sí, para la creación de instancias sincrónicas parece arriesgado: si se permite await , es extraño que alguien llame a las exportaciones mientras está en pausa. Rechazar await puede ser lo más simple y seguro. (Quizás también en el inicio asíncrono por consistencia, ¿no parece haber casos de uso importantes que lo impidan? Necesita pensar más).

(que se usa en los trabajadores)?

Hmm buen punto; No creo que tenga que usarse en Workers, pero dado que esta API ya existe, ¿quizás podría devolver una Promesa? He visto esto como un patrón emergente semipopular para devolver los elementos de un constructor de varias bibliotecas, aunque no estoy seguro de si es una buena idea hacerlo en una API estándar.

Estoy de acuerdo en no permitirlo en start (como en la captura) es lo más seguro por ahora, y siempre podemos cambiar eso en el futuro de una manera compatible con versiones anteriores en caso de que algo cambie.

Tal vez me perdí algo, pero no hay discusión sobre lo que sucede cuando la ejecución de WASM se detiene con una instrucción await y se devuelve una promesa a JS, luego JS vuelve a llamar a WASM sin esperar la promesa.

¿Es ese un caso de uso válido? Si es así, entonces podría permitir que las aplicaciones de "bucle principal" reciban eventos de entrada sin ceder el paso al navegador manualmente. En cambio, podrían ceder esperando una promesa que se resuelva de inmediato.

¿Qué pasa con la cancelación? No está implementado en las promesas de JS y esto causa algunos problemas.

@Kangz

Tal vez me perdí algo, pero no hay discusión sobre lo que sucede cuando la ejecución de WASM se detiene con una instrucción de espera y se devuelve una promesa a JS, luego JS vuelve a llamar a WASM sin esperar la promesa.

¿Es ese un caso de uso válido? Si es así, entonces podría permitir que las aplicaciones de "bucle principal" reciban eventos de entrada sin ceder el paso al navegador manualmente. En cambio, podrían ceder esperando una promesa que se resuelva de inmediato.

El texto actual quizás no sea lo suficientemente claro al respecto. Para el primer párrafo, sí, eso está permitido, consulte la sección "Aclaraciones": 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.

Para el segundo párrafo, no: no puede obtener eventos antes y no puede hacer que JS resuelva una Promesa antes de lo que lo haría. Permítanme tratar de resumir las cosas de otra manera:

  • Cuando wasm se detiene en la Promesa A, vuelve a salir a lo que sea que lo haya llamado y devuelve una nueva Promesa B.
  • Wasm se reanuda cuando se resuelve la Promesa A. Eso sucede a la hora normal , lo que significa que todo es normal en el ciclo de eventos JS.
  • Después de que wasm se reanuda y también termina de ejecutarse, solo entonces se resuelve Promise B.

Entonces, en particular, la Promesa B tiene que resolverse después de la Promesa A. No puede obtener el resultado de la Promesa A antes de que JS pueda obtenerlo.

Para decirlo de otra manera: el comportamiento de esta propuesta puede ser polillenado por Asyncify + algún JS que use Promises a su alrededor.

@RReverser , no creo que sean lo mismo, pero primero creo que debemos aclarar algo (si aún no se ha aclarado, en cuyo caso lamento haberlo perdido).

Puede haber varias llamadas de JS a la misma instancia de wasm en la misma pila al mismo tiempo. Si la instancia ejecuta await , ¿qué llamada se detiene y devuelve una promesa?

Para el segundo párrafo, no: no puede obtener eventos antes y no puede hacer que JS resuelva una Promesa antes de lo que lo haría.

Lo siento, creo que mi pregunta no fue clara. Por el momento, las aplicaciones de "bucle principal" en C++ usan emscripten_set_main_loop para que, entre cada ejecución de la función de marco, el control vuelva al navegador y se puedan procesar entradas u otros eventos.

Con esta propuesta, parece que lo siguiente debería funcionar para traducir aplicaciones de "bucle principal". (aunque no conozco bien el bucle de eventos JS)

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

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

@Kangz Eso debería funcionar, sí (excepto que tiene un pequeño problema con el orden de los argumentos en su código setTimeout y además podría simplificarse):

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

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

Puede haber varias llamadas de JS a la misma instancia de wasm en la misma pila al mismo tiempo. Si la instancia ejecuta await, ¿qué llamada se detiene y devuelve una promesa?

El más interior. Es trabajo del contenedor JS coordinar el resto si así lo desea.

@Kangz Lo siento, te entendí mal antes de eso. Sí, como @RReverser dijo que debería funcionar, ¡y es un buen ejemplo de un caso de uso previsto aquí!

Como dijiste, es policompletable con Asyncify y, de hecho, es equivalente al mismo código con Asyncify hoy al reemplazar __builtin_await con una llamada a emscripten_sleep(0) (que hace un setTimeout(0) ) .

Gracias, @RReverser , por la aclaración. Creo que sería útil reformular la descripción para decir que la llamada (más reciente) a la instancia se detiene, en lugar de la instancia misma.

En ese caso, esto suena casi equivalente a agregar las siguientes dos funciones primitivas a JS: promise-on-await(f) y await-for-promise(p) . El primero llama a f() pero, si durante la ejecución de f() se realiza una llamada a await-for-promise(p) , en su lugar devuelve una nueva Promesa que reanuda la ejecución después de que se resuelva p y se resuelve una vez que se completa la ejecución (o llama a await-for-promise nuevamente). Si se realiza una llamada a await-for-promise en el contexto de varios promise-on-await , la más reciente devuelve una Promesa. Si se realiza una llamada a await-for-promise fuera de cualquier promise-on-await , entonces sucede algo malo (al igual que si el código start una instancia ejecuta await ).

¿Tiene sentido?

@RossTate Eso está bastante cerca, sí, y capta la idea general. (Pero como dijiste, solo casi equivalente, ya que no se pudo usar para polillenar esto, y le falta el manejo de límites específico de wasm/JS).

Gracias por la sugerencia de reformular ese texto. Estoy guardando una lista de tales notas de la discusión aquí. (No estoy seguro de si vale la pena aplicarlos en la primera publicación, ya que parece menos confuso no cambiarlo con el tiempo).

@RossTate Interesante... ¡Me gusta esto! Hace explícita la naturaleza asíncrona de la llamada (se requiere promise-on-await para cualquier llamada potencialmente asíncrona) y no requiere ningún cambio en Wasm. También tiene (algo) sentido si elimina Wasm del medio: si promise-on-await llama a await-for-promise directamente, entonces devuelve Promise .

@kripken , ¿puede entrar en más detalles sobre por qué esto sería diferente? No entiendo muy bien por qué el límite Wasm/JS es importante aquí.

@binji Solo quise decir que tales funciones en JS no permitirían que wasm hiciera algo similar. Llamarlos como importaciones de wasm no funcionaría. Todavía necesitamos una forma de hacer que wasm salga al límite, etc. de una manera reanudable, ¿no es así?

@kripken correcto, supongo que en ese momento la importación await-for-promise debería estar funcionando como un intrínseco de Wasm.

Pensé que, en lugar de agregar una instrucción await a wasm, dicho módulo importaría await-for-promise y lo llamaría. De manera similar, en lugar de cambiar las funciones exportadas, el código JS las llamaría dentro de promise-on-await . Esto significa que las primitivas de JS manejarían todo el trabajo de la pila, incluida la pila de WebAssembly. También sería más flexible, por ejemplo, si lo desea, puede darle al módulo una devolución de llamada JS que luego puede volver a llamar al módulo y hacer que la llamada externa haga una pausa en lugar de la cláusula interna --- todo depende de si el código JS elige para envolver la llamada en promise-on-await o no. No creo que necesites cambiar nada en wasm.

Me interesaría saber qué piensa @syg sobre estas posibles primitivas de JS.

Oh, está bien, lo siento: tomé tu comentario @RossTate como "para asegurarme de que entiendo, déjame reformularlo así y dime si tiene la forma correcta", y no una sugerencia concreta.

Pensando en ello, su idea quiere pausar no solo los marcos JS sino también wasm, pero también hay marcos de host/navegador. (La propuesta actual evita eso al trabajar solo en wasm hasta el límite donde se llamó). Aquí hay un ejemplo:

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

Si se implementa forEach en el código del navegador, significa pausar los marcos del navegador. También es significativo que hacer una pausa en medio de un ciclo de este tipo y reanudarlo más tarde sería un nuevo poder que JS puede hacer, y su idea también permitiría eso para un ciclo normal:

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

Y todo esto puede tener curiosas interacciones de especificaciones con las funciones JS async . Todos estos parecen grandes debates para tener con el navegador y la gente de JS.

Pero también, esto solo evita agregar await y waitref en la especificación central de wasm, pero esas son pequeñas adiciones, ya que no hacen nada en la especificación central. La propuesta actual ya tiene el 99% de la complejidad en el lado JS. Y IIUC, su propuesta compensa esa pequeña adición a la especificación de wasm con adiciones mucho más grandes en el lado de JS, por lo que hace que la plataforma web en su conjunto sea más compleja, e innecesariamente, ya que todo esto es para wasm. Además, en realidad hay un beneficio al definir await en la especificación central de wasm, que puede ser útil fuera de la Web.

Tal vez me he perdido algo en tu sugerencia, disculpa si es así. En general, tengo curiosidad por saber cuál es su motivación para tratar de evitar una adición a la especificación central de wasm.

No creo que esas primitivas tengan mucho sentido para js, y creo que más implementaciones de wasm que las de los navegadores pueden beneficiarse de esto. Todavía tengo curiosidad por qué las excepciones reanudables (más o menos efectos) no cumplirían con este caso de uso.

Mi comentario fue una combinación de ambos. En un nivel alto, estoy tratando de averiguar si hay una manera de reformular la propuesta como un mero enriquecimiento de la API de JS (y de manera similar, cómo otros hosts interactuarían con los módulos wasm). El ejercicio ayuda a evaluar si wasm realmente necesita ser cambiado y ayuda a determinar si realmente la propuesta está agregando secretamente nuevas primitivas a JS que la gente de JS puede aprobar o no. Es decir, si no es posible hacerlo solo con un await : func (param externref) (result externref) importado, entonces es muy probable que esto esté agregando una nueva funcionalidad a JS.

En cuanto a la simplicidad de los cambios en wasm, todavía hay muchas cosas que considerar, como qué hacer con las llamadas de módulo a módulo, qué hacer cuando las funciones exportadas devuelven valores de GC que contienen punteros a funciones que pueden ejecutar await después de que termine la llamada, y así sucesivamente.

Volviendo al ejercicio, como señaló, hay buenas razones para capturar solo la pila wasm. Esto me lleva de vuelta a mi sugerencia anterior, aunque ligeramente revisada con una nueva perspectiva. Agregue una función WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2") a la API de JS. Supongamos que moduleBytes tiene una cantidad de importaciones más una importación adicional import "name1" "name2" (func (param externref) (result externref)) . Luego instantiateAsync instancia las otras importaciones de moduleBytes simplemente con los valores dados por imports e instancia la importación adicional con lo que es conceptualmente await-for-promise . Cuando las funciones exportadas se crean a partir de esta instancia, se protegen (conceptualmente por promise-on-await ) de modo que cuando se llama a este await-for-promise sube por la pila para encontrar el primer protector y luego copia el contenido de la pila en una nueva Promesa que luego se devuelve inmediatamente. Ahora tenemos las mismas primitivas que mencioné anteriormente, pero ya no son de primera clase, y este patrón restringido garantiza que solo se capturará la pila wasm. Al mismo tiempo, no es necesario cambiar WebAssembly para admitir el patrón.

¿Pensamientos?

@devsnek

Todavía tengo curiosidad por qué las excepciones reanudables (más o menos efectos) no cumplirían con este caso de uso.

Son una opción en este espacio, seguro.

Mi entendimiento de la última presentación de @rossberg es que inicialmente quería seguir ese camino, pero luego cambió de dirección para hacer un enfoque de rutina. Vea la diapositiva con el título "Problemas". Después de esa diapositiva, se describen las rutinas, que son otra opción en este espacio. Entonces, tal vez su pregunta sea más para @rossberg , ¿quién quizás pueda aclarar?

Esta propuesta está enfocada a resolver el problema sync/async el cual no requiere tanta potencia como las excepciones reanudables o las corrutinas. Esos se enfocan en las interacciones internas dentro de un módulo wasm, mientras que nosotros nos enfocamos en la interacción entre un módulo wasm y el exterior (porque ahí es donde ocurre el problema de sincronización/asincronía). Es por eso que solo necesitamos una sola instrucción nueva en la especificación central de wasm, y casi toda la lógica en esta propuesta está en la especificación wasm JS. Y significa que puedes esperar una Promesa como esta:

call $get_promise
await
;; use it!

Esa simplicidad en el wasm es útil por sí misma, pero también significa que está muy claro para la máquina virtual lo que está sucediendo, lo que también puede tener beneficios.

@RossTate

Es decir, si no es posible hacerlo solo con un await importado: func (param externref) (result externref), entonces es muy probable que esto esté agregando una nueva funcionalidad a JS.

No sigo esa inferencia, lo siento. Pero me parece un rodeo. Si cree que esta propuesta agrega una nueva funcionalidad a JS, ¿por qué no mostrarla directamente? (Creo firmemente que no, ¡pero tengo curiosidad si descubre que cometimos un error!)

En cuanto a la simplicidad de los cambios en wasm, todavía hay muchas cosas que considerar, como qué hacer con las llamadas de módulo a módulo.

¿La especificación central de wasm dice algo sobre las llamadas de módulo a módulo? No recuerdo que lo hiciera, y ahora que hojeo las secciones relevantes no veo eso. Pero tal vez me perdí algo?

Mi creencia es que las adiciones de especificaciones principales de wasm serían básicamente para enumerar await , decir que está destinado a "esperar algo", y eso es todo. Por eso escribí That's it for the core wasm spec! en la propuesta. Si me equivoco, muéstrenme en la especificación central de wasm dónde deberíamos agregar más.

Especulemos y digamos que algún día la especificación central de wasm tendrá una nueva instrucción para crear un módulo wasm y llamar a un método en él. En ese caso, me imagino que diríamos await solo trampas porque el objetivo es esperar algo en el exterior, en el host.

Esto me lleva de vuelta a mi sugerencia anterior, aunque ligeramente revisada con una nueva perspectiva [nueva idea]

¿Esa idea no es funcionalmente igual que el segundo párrafo en Alternative approaches considered en la propuesta? Tal cosa se puede hacer, pero explicamos por qué pensamos que es menos bueno.

@kripken lo entendió. para ser claros, creo que await resuelve los casos de uso presentados de una manera muy práctica y elegante. También espero que podamos usar este impulso para resolver otros casos de uso al ampliar un poco el diseño.

Creo que la sugerencia de @RossTate se parece mucho a lo que se menciona en "Enfoques alternativos considerados". Así que creo que deberíamos discutir con más detalle por qué se descartó ese enfoque. Creo que todos podemos estar de acuerdo en que sería preferible una solución que no implicara cambios en las especificaciones de wasm, si podemos hacer que el lado JS funcione. Estoy tratando de comprender las desventajas que presenta en esa sección y por qué hacen que la solución solo para JS sea tan inaceptable.

Creo que todos podemos estar de acuerdo en que sería preferible una solución que no implicara cambios en las especificaciones de wasm.

¡No! Vea los casos de uso fuera de la Web discutidos aquí. Sin await en la especificación de wasm, terminaríamos con cada plataforma haciendo algo ad-hoc: el entorno JS hace algo de importación, otros lugares crean nuevas API marcadas como "sincrónicas", etc. El ecosistema de wasm ser menos consistente, sería más difícil mover un wasm de la Web a otros lugares, etc.

Pero sí, deberíamos hacer que la parte central de las especificaciones de wasm sea lo más simple posible. Creo que esto hace eso? El 99% de la lógica está del lado de JS (pero @RossTate parece no estar de acuerdo, y todavía estamos tratando de resolverlo; hice preguntas concretas en mi última respuesta que espero que ayuden a avanzar).

Mi creencia es que las adiciones de especificaciones principales de wasm serían básicamente para enumerar await , decir que está destinado a "esperar algo", y eso es todo.

A menos que esta semántica se pueda formalizar con mayor precisión, esto introduce ambigüedad o comportamiento definido por la implementación en la especificación. Hasta ahora hemos evitado eso (a un costo significativo en el caso de SIMD), por lo que definitivamente es algo que me gustaría ver definido. No creo que la propuesta en sí tenga que cambiar para que sea más formal, pero "esperar algo" debería reformularse con la terminología precisa que ya se usa en la especificación.

¿La especificación central de wasm dice algo sobre las llamadas de módulo a módulo?

Las importaciones de una instancia se pueden instanciar con las exportaciones de otra instancia. Por lo que entiendo de la API JS (y del principio de composicionalidad de wasm), una llamada a tal importación es conceptualmente una llamada directa a cualquier función exportada por la otra instancia. Lo mismo ocurre con las llamadas (indirectas) a valores funcionales como funcref que se pasan entre las dos instancias.

Especulemos y digamos que algún día la especificación central de wasm tendrá una nueva instrucción para crear un módulo wasm y llamar a un método en él. En ese caso, me imagino que diríamos esperar solo trampas porque el objetivo es esperar algo en el exterior, en el host.

Basado en el principio de composicionalidad del módulo discutido en la reunión en persona, no debería atrapar. Debería ser como si hubiera una sola instancia de módulo (compuesta) y se ejecutara await . Es decir, await empaquetaría la pila hasta el marco de pila JS más reciente.

Tenga en cuenta que esto implica que si f fuera el valor de una función unaria exportada de alguna instancia de wasm, entonces el objeto de parámetros de creación de instancias {"some" : {"import" : f}} sería semánticamente diferente de {"some" : {"import" : (x) => f(x)}} porque las llamadas a la primera permanecerá dentro de la pila wasm mientras que las llamadas a la última entrarán en la pila JS, aunque apenas. Hasta ahora, estos objetos de parámetros de instanciación se considerarían equivalentes. Puedo explicar por qué eso es útil desde el punto de vista de migración de código/interoperabilidad de lenguaje, pero eso sería una digresión en este momento.

¿Esa idea no es funcionalmente la misma que el segundo párrafo en Enfoques alternativos considerados en la propuesta? Tal cosa se puede hacer, pero explicamos por qué pensamos que es menos bueno.

Lo siento, leí esa alternativa como algo diferente, pero eso no importa ahora excepto para explicar mi confusión. Parece que quisiste decir lo mismo que mi sugerencia, en cuyo caso vale la pena discutir los pros y los contras.

El hecho de que esta propuesta sea tan liviana en el lado de wasm se debe a que la instrucción await parece ser semánticamente idéntica a una llamada a una función importada. ¡Por supuesto, las convenciones importan, como usted señala! Pero await no es la única funcionalidad para la que esto es válido; lo mismo es cierto para la mayoría de las funciones importadas. En el caso de await , mi sensación es que la preocupación por la convención podría abordarse haciendo que los módulos con esta funcionalidad tengan una cláusula import "control" "await" (func (param externref) (result externref)) , y tener entornos que admitan esta funcionalidad siempre instancian esa importación con la devolución de llamada adecuada.

Eso parece brindar una solución que ahorra una tonelada de trabajo al no cambiar wasm y al mismo tiempo brindar la portabilidad multiplataforma que está buscando. Pero todavía estoy trabajando para comprender los matices de la propuesta, ¡y ya me he perdido un montón hasta ahora!

El hecho de que esta propuesta sea tan liviana en el lado de wasm se debe a que la instrucción await parece ser semánticamente idéntica a una llamada a una función importada.

FWIW, aquí es donde comenzó originalmente esta propuesta, pero usar intrínsecos como ese parece más opaco para las máquinas virtuales y, en general, se desaconseja (creo que @binji sugirió alejarse de él en las discusiones originales).

Por ejemplo, siguiendo su argumento, algo como memory.grow o atomic.wait también se podría hacer como import "control" "memory_grow" o import "control" "atomic_wait" correspondientemente, pero no son como son. No proporciona el mismo nivel de interoperabilidad y oportunidades de análisis estático (tanto en la VM como en el lado de las herramientas) como instrucción real.

Podría argumentar que memory.grow como instrucción sigue siendo útil para los casos en los que la memoria no se exporta, pero atomic.wait definitivamente podría implementarse fuera del núcleo. De hecho, es muy similar a await , excepto por el nivel en el que ocurre la pausa/reanudación y por el hecho de que await como función requeriría mucha más magia que atomic.wait ya que necesita poder interactuar con la pila de VM y no solo bloquear el hilo actual hasta que cambie un valor.

@tlively

"esperar algo" debe reformularse con la terminología precisa ya utilizada por la especificación.

Definitivamente, sí. Puedo sugerir un texto más específico ahora si eso fuera útil:

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.

Por cierto, otra comparación que se me ocurrió mientras escribía esto son las sugerencias de alineación en las cargas y las tiendas. Wasm admite cargas y almacenes no alineados, por lo que las sugerencias no pueden conducir a un comportamiento diferente observable por el módulo wasm (incluso si la sugerencia es incorrecta), pero para el host sugieren una implementación muy diferente en ciertas plataformas (que puede ser más eficiente). Así que ese es un ejemplo de diferentes instrucciones sin una semántica diferente observable internamente, como dice la especificación: The alignment in load and store instructions does not affect the semantics .

@RossTate

Basado en el principio de composicionalidad del módulo discutido en la reunión en persona, no debería atrapar. Debería ser como si solo hubiera una instancia de módulo (compuesta) y se ejecutara en espera. Es decir, await empacaría la pila hasta el marco de pila JS más reciente.

Suena bien, y es bueno saberlo, gracias, me perdí esa parte.

Creo que esto me explica parte de nuestro malentendido. Módulo => las llamadas de módulo no están en el cajero automático de especificaciones de wasm, que era mi punto anterior. Pero parece que está pensando en una especificación futura donde podrían estar. En cualquier caso, eso no parece ser un problema aquí, ya que la composición determina exactamente cómo debe comportarse un await en esa situación (¡que no es lo que sugerí antes! pero tiene más sentido).

¿La especificación central de wasm dice algo sobre las llamadas de módulo a módulo? No recuerdo que lo hiciera, y ahora que hojeo las secciones relevantes no veo eso. Pero tal vez me perdí algo?

Sí, la especificación central de wasm distingue entre funciones que se han importado de otros módulos de wasm y funciones de host (§ 4.2.6). La semántica de la invocación de funciones (§ 4.4.7) no depende del módulo que definió la función y, en particular, actualmente se especifica que las llamadas a funciones entre módulos se comporten de manera idéntica a las llamadas a funciones del mismo módulo.

Si await s debajo de las llamadas entre módulos se definen para atrapar, esto requeriría especificar un recorrido en la pila de llamadas para inspeccionar si existe una llamada entre módulos antes del último marco ficticio creado por una invocación del host. (§ 4.5.5). Esta sería una complicación desafortunada en la especificación. Pero estoy de acuerdo con Ross en que tener trampas de llamadas entre módulos sería una violación de la composicionalidad, por lo que preferiría la semántica en la que toda la pila se congela hasta la última invocación del host. La forma más sencilla de especificar eso sería hacer que await sea similar a la invocación de una función de host (§ 4.4.7.3), como usted dice, @kripken. Pero las invocaciones de funciones del host son completamente no deterministas, por lo que un mejor nombre para la instrucción desde el punto de vista de la especificación central podría ser undefined . Y en este punto, en realidad empiezo a preferir una importación intrínseca que siempre proporcionará la plataforma web (y WASI para la portabilidad) porque la especificación principal, por sí sola, no se beneficia de tener una instrucción undefined IMO.

Semánticamente, una llamada al entorno host que devuelve un waitref más un await es solo una llamada de bloqueo, ¿verdad?

¿Qué valor proporciona esto a las incrustaciones no web que no tienen un entorno asíncrono como lo tiene un navegador y que pueden admitir de forma nativa el bloqueo de llamadas?

@RReverser , veo el punto que estás haciendo sobre los intrínsecos. Hay una llamada de juicio involucrada en decidir cuándo una operación debe definirse a través de funciones no interpretadas versus instrucciones. Creo que un factor en este juicio es considerar cómo interactúa con otras instrucciones. memory.grow afecta el comportamiento de otras instrucciones de memoria. No he tenido la oportunidad de leer detenidamente la propuesta de Threads, pero imagino que atomic.wait afecta o se ve afectado por el comportamiento de otras instrucciones de sincronización. Luego, la especificación debe actualizarse para formalizar estas interacciones.

Pero con await solo, no hay interacciones con otras instrucciones. Las únicas interacciones son con el host, por lo que mi intuición sería que esta propuesta debería hacerse a través de funciones de host importadas.

Creo que una gran diferencia entre atomic.wait y este await propuesto es que no se puede volver a ingresar al módulo con atomic.wait . El agente queda suspendido en su totalidad.

@kripken :

Mi entendimiento de la última presentación de @rossberg es que inicialmente quería seguir ese camino, pero luego cambió de dirección para hacer un enfoque de rutina. Vea la diapositiva con el título "Problemas". Después de esa diapositiva, se describen las rutinas, que son otra opción en este espacio. Entonces, tal vez su pregunta sea más para @rossberg , ¿quién quizás pueda aclarar?

Sí, por lo que la factorización corrutinaria se puede considerar como una generalización del diseño anterior de excepciones reanudables. Todavía tiene la misma noción de eventos/excepciones reanudables, pero la instrucción try se descompone en primitivas más pequeñas, lo que simplifica la semántica y hace que el modelo de costos sea más explícito. También es algo más expresivo.

La intención sigue siendo que esto pueda expresar todas las abstracciones de control relevantes, y asíncrono es uno de los casos de uso motivadores. Para interoperar con JS asíncrono, la API de JS presumiblemente podría proporcionar un evento await predefinido (que lleva una promesa de JS como una referencia externa) que un módulo Wasm podría importar y throw suspender. Por supuesto, hay muchos detalles que deberían desarrollarse, pero en principio eso debería ser posible.

En cuanto a la propuesta actual, todavía estoy tratando de entenderla. :)

En particular, parece permitir await en cualquier función antigua de Wasm, ¿lo estoy leyendo correctamente? Si es así, eso es muy diferente de JS, que permite await solo en funciones asíncronas. ¡Y esa es una restricción muy central, porque permite que los motores compilen await mediante la transformación _local_ de una sola función (asincrónica)!

Sin esa restricción, los motores tendrían que realizar una transformación de programa _global_ (como supuestamente lo hace Asyncify), donde cada llamada sería potencialmente mucho más costosa (generalmente no se puede saber si alguna llamada podría alcanzar una espera). O, de manera equivalente, ¡los motores deberían poder crear varias pilas y cambiar entre ellas!

Ahora bien, esta es exactamente la función que la idea de los controladores de corrutina/efecto intenta introducir en Wasm. Pero obviamente, es una adición muy no trivial a la plataforma y su modelo de ejecución, una complicación que JS ha tenido mucho cuidado de evitar para sus abstracciones de control (como asíncrono y generadores).

@rossberg

En particular, parece permitir la espera en cualquier función antigua de Wasm, ¿lo estoy leyendo correctamente? Si es así, eso es muy diferente de JS, que permite esperar solo en funciones asíncronas.

Sí, el modelo aquí es muy diferente. JS await es por función, mientras que esta propuesta hace una espera de una instancia completa de wasm (porque el objetivo es resolver el desajuste de sincronización/asincronía entre JS y wasm, que es entre JS y wasm). También JS await es para código escrito a mano, mientras que esto es para permitir la transferencia de código compilado.

¡Y esa es una restricción muy central, porque permite que los motores compilen en espera mediante la transformación local de una sola función (asincrónica)! Sin esa restricción, los motores tendrían que realizar una transformación de programa global (como supuestamente lo hace Asyncify), donde cada llamada sería potencialmente mucho más costosa (generalmente no se puede saber si alguna llamada podría alcanzar una espera). O, de manera equivalente, ¡los motores deberían poder crear varias pilas y cambiar entre ellas!

¡Definitivamente no se pretende aquí una transformación global del programa! Lo siento si eso no fue claro.

Como se menciona en la propuesta, el cambio entre pilas es una posible opción de implementación, pero tenga en cuenta que no es lo mismo que el cambio de pila al estilo corrutina:

  • Solo se puede hacer una pausa en toda la instancia de wasm. Esto no es para el cambio de pila dentro del módulo. (En particular, es por eso que esta propuesta no podría tener adiciones a la especificación central de wasm y estar completamente del lado de wasm JS; hasta ahora, algunas personas prefieren eso, y creo que cualquier forma puede funcionar).
  • Las corrutinas declaran pilas explícitamente, await no.
  • Las pilas de espera solo se pueden reanudar una vez, no hay bifurcación/retorno más de una vez (¿no está seguro si tendrá eso en su propuesta o no?).
  • El modelo de rendimiento es muy diferente aquí. await va a esperar en una Promesa en JS, que ya tiene una sobrecarga y una latencia mínimas. Por lo tanto, está bien si la implementación tiene algunos gastos generales cuando hacemos una pausa, y nos importa menos de lo que probablemente lo harían las corrutinas.

Dados esos factores, y que el comportamiento observable de esta propuesta es que una instancia completa de wasm se detiene, puede haber varias formas de implementarla. Por ejemplo, fuera de la web en una máquina virtual que ejecuta una sola instancia de wasm, podría literalmente ejecutar su ciclo de eventos hasta que sea el momento de reanudar el wasm. En la Web, un enfoque de implementación podría ser: cuando ocurre una espera, copiar toda la pila de wasm, desde la posición actual hasta donde llamamos al wasm; guardar eso en el lado; para reanudar, cópielo nuevamente y continúe desde allí. También puede haber otros enfoques o variaciones de estos (algunos quizás sin copiar, pero nuevamente, ¡evitar la sobrecarga de copia no es realmente crucial aquí!).

Perdón por la publicación larga y algunas repeticiones del texto de la propuesta en sí, pero espero que esto ayude a aclarar algunos de los puntos a los que se refirió.

Creo que hay mucho que discutir aquí en términos de implementación. ¡Hasta ahora, el comentario de @acfoltzer sobre Lucet es alentador!

Solo para aclarar algunas frases en el comentario más reciente de @kripken , no es toda la instancia de wasm la que se detiene. Es solo la llamada más reciente de un marco de host a wasm en la pila que está en pausa, y luego se devuelve al marco de host una promesa correspondiente (o el análogo apropiado para el host). Ver aquí para la aclaración anterior relevante.

Hm, no veo cómo eso hace la diferencia. Cuando espere en algún lugar profundo dentro de Wasm, deberá capturar toda la pila de llamadas desde al menos la entrada del host hasta ese punto. Y puede mantener viva esa suspensión (es decir, ese segmento de pila) durante el tiempo que desee, mientras realiza otras llamadas desde arriba o crea más suspensiones. Y puede reanudar desde otro lugar (¿creo?). ¿No requiere eso toda la maquinaria de implementación de continuaciones delimitadas? Solo que el indicador se establece en la entrada de Wasm en lugar de una construcción separada.

@rossberg

Eso podría ser cierto en algunas máquinas virtuales, sí. Si await y coroutines terminan necesitando exactamente el mismo trabajo de VM, entonces al menos no se necesita trabajo adicional. En ese caso, el beneficio de la propuesta de espera sería la conveniente integración de JS.

Creo que puede obtener una integración JS conveniente sin una transformación completa del programa si no permite que se vuelva a ingresar al módulo.

Creo que puede obtener una integración JS conveniente sin una transformación completa del programa si no permite que se vuelva a ingresar al módulo.

Esto parece una forma más fácil de hacerlo, pero eso requeriría bloquear cualquier módulo visitado en la pila de llamadas (o como primer paso, todos los módulos de WebAssembly).

Esto parece una forma más fácil de hacerlo, pero eso requeriría bloquear cualquier módulo visitado en la pila de llamadas (o como primer paso, todos los módulos de WebAssembly).

Correcto, como atomic.wait .

@taralx

Creo que puede obtener una integración JS conveniente sin una transformación completa del programa si no permite que se vuelva a ingresar al módulo.

Por un lado, el reingreso puede ser útil, por ejemplo, un motor de juego puede descargar un archivo y no querer que la interfaz de usuario se detenga por completo mientras lo hace (Asyncify lo permite hoy). Pero, por otro lado, tal vez no se permita el reingreso, pero una aplicación podría crear varias instancias del mismo módulo para eso (¿todas importando la misma memoria, globales mutables, etc.?), por lo que un reingreso sería una llamada a otra instancia. Creo que podemos hacer que eso funcione en cadenas de herramientas (habría un límite efectivo en la cantidad de reingresos activos a la vez, igual a la cantidad de instancias, lo que parece correcto).

Entonces, si su simplificación ayudaría a las máquinas virtuales, ¡definitivamente vale la pena considerarlo!

(Sin embargo, tenga en cuenta que, como se discutió anteriormente, no creo que necesitemos una transformación completa del programa aquí con cualquiera de las opciones que se discuten. Solo necesita eso si se encuentra en la mala situación en la que se encuentra Asyncify, donde es todo lo que puede hacer en el nivel de cadena de herramientas. Para esperar, en el peor de los casos, como se discutió con @rossberg , puede hacer lo que la propuesta de coroutines haría internamente. ¡Pero su idea es potencialmente muy interesante si hace las cosas más simples que eso!)

Por un lado, el reingreso puede ser útil, por ejemplo, un motor de juego puede descargar un archivo y no querer que la interfaz de usuario se detenga por completo mientras lo hace (Asyncify lo permite hoy).

Sin embargo, no estoy seguro de que esta sea una función de sonido. Sin embargo, me parece que esto introduciría _concurrencia inesperada_ en la aplicación. Una aplicación nativa que cargue activos durante la representación usaría 2 subprocesos internamente, y cada subproceso se asignaría a un WebWorker + SharedArrayBuffer. Si una aplicación usa subprocesos, también podría usar primitivos web síncronos de WebWorkers (como están permitidos, al menos en algunos casos). De lo contrario, siempre es posible asignar operaciones asíncronas en el hilo principal a operaciones de bloqueo en un trabajador usando Atomics.wait (por ejemplo).

Me pregunto si todo el caso de uso no está ya resuelto por subprocesos múltiples en general. Al usar primitivos de bloqueo en un trabajador, se conserva toda la pila (JS/Wasm/navegador nativo), lo que parece ser mucho más simple y robusto.

Al usar primitivos de bloqueo en un trabajador, se conserva toda la pila (JS/Wasm/navegador nativo), lo que parece ser mucho más simple y robusto.

Esa es en realidad otra implementación alternativa del contenedor Asyncify JS independiente con el que he experimentado, pero, si bien resuelve el problema del tamaño del código, la sobrecarga de rendimiento fue incluso mucho mayor que la actual Asyncify que usa la transformación Wasm.

@alexp-sssup

Sin embargo, me parece que esto introduciría una concurrencia inesperada en la aplicación.

Definitivamente sí, debe hacerse con mucho cuidado y puede romper cosas. Tenemos experiencias mixtas con esto usando Asyncify, buenas y malas (para un ejemplo de caso de uso válido: un archivo se descarga en JS, y JS llama a wasm para encontrar espacio en el que copiarlo, antes de reanudar). Pero en cualquier caso, el reingreso no es una parte crucial de esta propuesta de ninguna manera.

Para agregar a lo que dijo @RReverser , otro problema con los subprocesos es que el soporte para ellos no es y no será universal. Pero await podría estar en todas partes.

En otros lenguajes donde se han introducido async/await, el reingreso es absolutamente clave. Es algo así como el punto de que otros eventos pueden ocurrir mientras uno está (a) esperando. Me parece que el reingreso es bastante importante.

Además, ¿no es cierto que cada vez que un módulo realiza una llamada a una función externa, debe suponer que se puede volver a ingresar a través de cualquiera de sus exportaciones (en el ejemplo anterior, incluso sin ninguna espera, cualquier llamada a una función externa La función es gratuita (sin juego de palabras) para llamar a malloc).

una aplicación podría crear múltiples instancias del mismo módulo para eso (¿todas importando la misma memoria, globales mutables, etc.?), por lo que un reingreso sería una llamada a otra instancia

Solo para las memorias compartidas del módulo. Las otras memorias deben volver a crear instancias, lo cual es importante para evitar que una operación pisotee a otra operación durante los cambios.

Observo que la versión no reentrante de esto se puede policompletar en cualquier incrustación con soporte de subprocesos, en caso de que alguien quisiera jugar con ella y ver qué tan útil es.

Observo que la versión no reentrante de esto se puede policompletar en cualquier incrustación con soporte de subprocesos, en caso de que alguien quisiera jugar con ella y ver qué tan útil es.

Como se mencionó anteriormente, eso es algo con lo que ya jugamos, pero lo descartamos porque brinda un rendimiento aún peor que la solución actual, no es universalmente compatible y, además, hace que sea muy difícil compartir WebAssembly.Global o WebAssembly.Table con el hilo principal sin trucos adicionales, por lo que es una mala elección para un polyfill transparente.

La solución actual que reescribe el módulo Wasm no sufre estos problemas, sino que tiene un costo de tamaño de archivo significativo.

Como tal, ninguno de estos es excelente para grandes aplicaciones del mundo real, lo que nos motiva a buscar soporte nativo para la integración asincrónica como se describe aquí.

peor rendimiento

¿Tiene algún tipo de punto de referencia?

Sí, puedo compartirlo cuando regrese al trabajo el martes (o, más probablemente, el miércoles), o es bastante fácil preparar uno que solo llame para vaciar la función JS asíncrona usted mismo.

Gracias. Podría crear un micropunto de referencia, pero no sería muy instructivo.

Ah, sí, el mío también es un micropunto de referencia, ya que estábamos interesados ​​únicamente en la comparación de gastos generales.

El problema con un microbenchmark es que no sabemos cuánta latencia es aceptable para una aplicación real. Si toma 1 ms adicional, ¿es eso realmente un problema si la aplicación solo realiza operaciones de espera a una velocidad de 1/s, por ejemplo?

Creo que el enfoque en la velocidad de un enfoque atómico puede ser una distracción. Como se mencionó anteriormente, los atómicos no funcionan y no funcionarán en todas partes (debido a COOP/COEP) y también solo un trabajador podría usar el enfoque atómico ya que el subproceso principal no puede bloquear. Es una buena idea, pero para una solución universal necesitamos algo como Await.

No lo estoy sugiriendo como una solución a largo plazo. Estoy sugiriendo que un polyfill que lo use podría usarse para ver si una solución no reentrante funcionará para las personas.

@taralx Oh, ok, ahora veo, gracias.

@taralx :

Creo que puede obtener una integración JS conveniente sin una transformación completa del programa si no permite que se vuelva a ingresar al módulo.

Eso sería malo. Significa que la fusión de varios módulos podría romper su comportamiento. Esa sería la antítesis de la modularidad.

Como principio general de diseño, el comportamiento operativo nunca debe depender de los límites del módulo (aparte del simple alcance). Los módulos son simplemente un mecanismo de agrupación y alcance en Wasm, y desea mantener la capacidad de reagrupar cosas (módulos de enlace/fusión/división) sin que eso cambie el comportamiento de un programa.

@rossberg : esto se puede generalizar como el bloqueo del acceso a cualquier módulo Wasm, como se propuso anteriormente. Pero entonces es probablemente demasiado limitante.

Eso sería malo. Significa que la fusión de varios módulos podría romper su comportamiento. Esa sería la antítesis de la modularidad.

Ese era mi punto con el argumento del polirrelleno: atomic.wait no rompe la modularidad, así que esto tampoco debería hacerlo.

@taralx , atomic.wait hace referencia a una ubicación específica en una memoria específica. ¿Qué memoria y ubicación usaría el bloqueo await y cómo se controlaría qué módulos comparten esa memoria?

@rossberg , ¿puede dar más detalles sobre un escenario que cree que esto rompe? Sospecho que tenemos ideas diferentes sobre cómo funcionaría la versión no reentrante.

@taralx , considere cargar dos módulos A y B, cada uno proporcionando alguna función de exportación, digamos A.f y B.g . Ambos pueden realizar await cuando se les llama. A dos piezas de código de cliente se les pasa una de estas funciones, respectivamente, y las llaman de forma independiente. No interfieren ni se bloquean entre sí. Luego, alguien fusiona o refactoriza A y B en C, sin cambiar nada en el código. De repente, ambas partes del código del cliente podrían comenzar a bloquearse entre sí de forma inesperada. Acción espeluznante a distancia a través de un estado compartido oculto.

Eso tiene sentido. Pero permitir el reingreso corre el riesgo de concurrencia en módulos que no lo esperan, por lo que es una acción espeluznante a distancia de cualquier manera.

Pero los módulos ya son reingresables, ¿no? Cada vez que un módulo realiza una llamada de importación, el código externo puede volver a ingresar al módulo, lo que podría cambiar el estado global antes de regresar. No puedo ver cómo el reingreso durante la espera propuesta es más espeluznante o concurrente que llamar a una función importada. ¿Quizás me estoy perdiendo algo?

(editado)

Hm, sí. Bien, entonces una función importada podría volver a ingresar al módulo. Claramente necesito pensar más sobre esto.

Cuando el código se está ejecutando y llama a una función, hay dos posibilidades: sabe que la función no llamará a cosas aleatorias, o que la función podría llamar a cosas aleatorias. En este último caso, la reentrada siempre es posible. Las mismas reglas se aplican a await .

(editado mi comentario de arriba)

¡Gracias a todos por la discusión hasta ahora!

Para resumir, parece que hay un interés general aquí, pero hay grandes preguntas abiertas como si esto debería estar 100% del lado de JS o solo 99%; parece que lo primero eliminaría las principales preocupaciones que tienen algunas personas, y eso estar bien para el caso de la Web, por lo que probablemente esté bien. Otra gran pregunta abierta es qué tan factible sería hacerlo en máquinas virtuales sobre las que necesitamos más información.

Sugeriré un tema de agenda para la próxima reunión del GC en 2 semanas para discutir esta propuesta y considerarla para la etapa 1, lo que significaría abrir un repositorio y discutir las preguntas abiertas en temas separados con más detalle allí. (Creo que ese es el proceso correcto, pero corríjame si me equivoco).

Solo para tu información
Vamos a armar una propuesta de cambio de pila completa de una manera similar
periodo de tiempo. Siento que eso podría hacer discutible su variante de caso especial -
¿Qué piensas?
Francisco

El jueves 28 de mayo de 2020 a las 3:51 p. m., Alon Zakai [email protected] escribió:

¡Gracias a todos por la discusión hasta ahora!

Para resumir, parece que hay un interés general aquí, pero hay
grandes preguntas abiertas como si esto debería ser 100% del lado de JS o simplemente
99%: parece que lo primero eliminaría las principales preocupaciones de algunas personas
tener, y eso estaría bien para el caso de la Web, por lo que probablemente esté bien.
Otra gran pregunta abierta es qué tan factible sería hacerlo en máquinas virtuales que
necesitamos más información sobre.

Sugeriré un tema de la agenda para la próxima reunión del GC en 2 semanas para discutir
esta propuesta y considerarla para la etapa 1, lo que significaría abrir un repo
y discutiendo las preguntas abiertas en números separados con más detalle allí.
(Creo que ese es el proceso correcto, pero corríjame si me equivoco).


Estás recibiendo esto porque estás suscrito a este hilo.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/WebAssembly/design/issues/1345#issuecomment-635649331 ,
o darse de baja
https://github.com/notifications/unsubscribe-auth/AAQAXUCLZ4CJVQYEUBK23BLRT3TFLANCNFSM4NEJW2PQ
.

>

Francisco McCabe
SUE

@fgmccabe

Deberíamos discutir eso con seguridad.

Sin embargo, en general, a menos que su propuesta se centre en el lado JS, supongo que no haría que esto fuera discutible (que es 99% -100% en el lado JS).

Ahora que ha concluido la discusión sobre los detalles de la implementación, me gustaría volver a plantear una inquietud de alto nivel que expresé anteriormente pero que descarté por el bien de tener una discusión a la vez.

Un programa se compone de muchos componentes. Desde una perspectiva de ingeniería de software, es importante que dividir componentes en partes o fusionar componentes no cambie significativamente el comportamiento del programa. Este es el razonamiento detrás del principio de composición de módulos discutido en la última reunión presencial de CG, y está implícito en el diseño de muchos lenguajes.

En el caso de los programas web, ahora con WebAssembly estos diferentes componentes pueden incluso estar escritos en diferentes lenguajes: JS o wasm. De hecho, muchos componentes también podrían estar escritos en cualquiera de los dos idiomas; Me referiré a estos como componentes "ambivalentes". En este momento, la mayoría de los componentes ambivalentes están escritos en JS, pero imagino que todos esperamos que más y más de ellos se reescriban en wasm. Para facilitar esta "migración de código", debemos tratar de asegurarnos de que reescribir un componente de esta manera no cambie la forma en que interactúa con el entorno. Como ejemplo de juguete, si un componente de programa de "aplicación" en particular (f, x) => f(x) está escrito en JS o en wasm no debería afectar el comportamiento del programa en general. Este es un principio de migración de código.

Desafortunadamente, todas las variantes de esta propuesta parecen violar el programa de composición de módulos o el principio de migración de código. El primero se viola cuando await captura la pila hasta donde se ingresó por última vez el módulo wasm actual, porque este límite cambia a medida que los módulos se separan o se combinan. Este último se viola cuando await captura la pila hasta donde se ingresó wasm más recientemente, porque este límite cambia a medida que el código se migra de JS a wasm (de modo que migrar algo tan simple como (f, x) => f(x) desde JS a wasm puede cambiar significativamente el comportamiento del programa en general).

No creo que estas violaciones se deban a malas decisiones de diseño de esta propuesta. Más bien, el problema parece ser que esta propuesta está tratando de evitar indirectamente que JS sea más poderoso, y ese objetivo lo obliga a imponer límites artificiales que violan estos principios. Entiendo totalmente ese objetivo, pero sospecho que este problema surgirá cada vez más: agregar funcionalidad a WebAssembly de una manera que respete estos principios a menudo requerirá agregar funcionalidad indirectamente a JS debido a que JS es el lenguaje de incrustación. Mi preferencia sería abordar ese problema de frente (que realmente no tengo idea de cómo resolverlo). Si no es así, entonces mi preferencia secundaria sería hacer este cambio únicamente en la API de JS, porque JS es el factor limitante aquí, en lugar de agregar instrucciones a WebAssembly para las que wasm no tiene interpretación.

No creo que estas violaciones se deban a malas decisiones de diseño de esta propuesta. Más bien, el problema parece ser que esta propuesta está tratando de evitar indirectamente hacer que JS sea más poderoso.

Eso es importante, pero no es la razón principal del diseño aquí.

La razón principal de este diseño es que, si bien estoy totalmente de acuerdo en que el principio de composición tiene sentido para wasm , el problema fundamental que tenemos en la Web es que, de hecho, JS y wasm no son equivalentes en la práctica. Tenemos JS escrito a mano que es asíncrono y wasm portado que es síncrono. En otras palabras, el límite entre ellos es en realidad el problema exacto que estamos tratando de abordar. En general, no estoy seguro de estar de acuerdo en que el principio de composición se deba aplicar a wasm y JS (pero tal vez debería ser un debate interesante).

Esperaba tener más discusión públicamente aquí, pero para ahorrar tiempo me comuniqué directamente con algunos implementadores de VM, ya que pocos se han involucrado aquí hasta ahora. Dados sus comentarios junto con la discusión aquí, lamentablemente creo que deberíamos hacer una pausa en esta propuesta.

Await tiene un comportamiento observable mucho más simple que las corrutinas generales o el cambio de pila, pero la gente de VM con la que hablé está de acuerdo con @rossberg en que el trabajo de VM al final probablemente sería similar para ambos. Y al menos algunas personas de VM creen que obtendremos corrutinas o cambio de pila de todos modos, y que podemos respaldar los casos de uso de await usando eso. Eso significará crear una nueva corrutina/pila en cada llamada al wasm (a diferencia de esta propuesta), pero al menos algunas personas de VM piensan que podría hacerse lo suficientemente rápido.

Además de la falta de interés de la gente de VM, hemos tenido algunas objeciones fuertes a esta propuesta aquí por parte de @fgmccabe y @RossTate , como se discutió anteriormente. No estamos de acuerdo en algunas cosas, pero aprecio esos puntos de vista y el tiempo que se dedicó a explicarlos.

En conclusión, en general parece que sería una pérdida de tiempo para todos tratar de avanzar aquí. ¡Pero gracias a todos los que participaron en la discusión! Y con suerte, al menos esto motiva a priorizar el cambio de rutinas/pilas.

Tenga en cuenta que la parte JS de esta propuesta puede ser relevante en el futuro, ya que JS sugar básicamente para la integración conveniente de Promise. Tendremos que esperar el cambio de pila o las corrutinas y ver si esto podría funcionar además de eso. Pero no creo que valga la pena mantener el tema abierto por eso, así que cerrando.

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

JimmyVV picture JimmyVV  ·  4Comentarios

Artur-A picture Artur-A  ·  3Comentarios

dpw picture dpw  ·  3Comentarios

cretz picture cretz  ·  5Comentarios

spidoche picture spidoche  ·  4Comentarios