Design: 建议:等待

创建于 2020-05-18  ·  96评论  ·  资料来源: WebAssembly/design

@rreverser和我想为 WebAssembly 提出一个新提案:等待

该提案的动机是帮助编译到 WebAssembly 的“同步”代码,这类似于从文件中读取:

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

这段代码不容易在主要是异步的主机环境中实现,并且会异步实现“从文件读取”,例如在 Web 上,

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

换句话说,目标是帮助解决 Web 上 wasm 中常见的同步/异步问题。

同步/异步问题是一个严重的问题。 虽然可以在编写新代码时牢记这一点,但大型现有代码库通常无法重构以解决它,这意味着它们无法在 Web 上运行。 我们确实有Asyncify ,它检测了一个 wasm 文件以允许暂停和恢复,并且它允许移植一些这样的代码库,所以我们在这里并没有完全被阻止。 然而,检测 wasm 有很大的开销,比如代码大小增加了 50%,平均速度降低了 50%(但有时更糟),因为我们添加了在本地状态中写出/读回的指令并调用堆栈和等等。 这种开销是一个很大的限制,它在许多情况下排除了 Asyncify!

该提案的目标是允许以一种有效的方式暂停和恢复执行(特别是,没有像 Asyncify 那样的开销),以便所有遇到同步/异步问题的应用程序都可以轻松避免它。 就个人而言,我们主要打算将其用于 Web,它可以帮助 WebAssembly 更好地与 Web API 集成,但 Web 之外的用例也可能是相关的。

简单的想法

这里的核心问题是在同步的 wasm 代码和异步的主机环境之间。 因此,我们的方法专注于 wasm 实例的边界和外部。 从概念上讲,当执行新的await指令时,wasm 实例“等待”来自外部的某些东西。 “等待”的含义在不同平台上会有所不同,并且可能并非在所有平台上都相关(就像并非所有平台都可能发现 wasm atomics 提案相关),但具体在 Web 平台上,wasm 实例将等待 Promise 并暂停直到解决或拒绝。 例如,一个 wasm 实例可以在fetch网络操作上暂停,并在.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

请注意在 JS 和其他语言中与await的一般相似性。 虽然这与它们不同(请参阅下面的详细信息),但主要好处是它允许编写看起来同步的代码(或者更确切地说,将看起来同步的代码编译成 wasm)。

细节

核心 wasm 规范

对核心 wasm 规范的更改非常小:

  • 添加waitref类型。
  • 添加await指令。

为每个await指令指定一个类型(如call_indirect ),例如:

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

该类型必须接收waitref ,并且可以返回任何类型(或不返回任何类型)。

await仅根据使主机环境执行某些操作来定义。 在这个意义上,它类似于unreachable指令,它在 Web 上使主机抛出RuntimeError ,但这不在核心规范中。 同样,核心 wasm 规范只说await是为了等待来自主机环境的某些东西,而不是我们实际这样做的方式,这在不同的主机环境中可能会有很大不同。

这就是核心 wasm 规范!

Wasm JS 规范

对 wasm JS 规范的更改(仅影响 Web 等 JS 环境)更有趣:

  • 一个有效的waitref值是一个 JS Promise。
  • 当在 Promise 上执行await时,整个 wasm 实例会暂停并等待该 Promise 解决或拒绝。
  • 如果 Promise 解决,实例在将从 Promise 接收到的值推送到堆栈后恢复执行(如果有的话)
  • 如果 Promise 拒绝,我们恢复执行并从await的位置抛出一个 wasm 异常。

“整个 wasm 实例暂停”是指保留所有本地状态(调用堆栈、本地值等),以便我们可以稍后恢复当前执行,就好像我们从未暂停过一样(当然全局状态可能已经改变,就像内存可能已被写入)。 在我们等待的同时,JS 事件循环正常运行,其他事情也可能发生。 当我们稍后恢复时(如果我们不拒绝 Promise,在这种情况下会抛出异常),我们会从中断的地方继续,基本上就好像我们从未暂停过一样(但与此同时,其他事情已经发生,并且全局状态可能已经改变等)。

当 JS 调用一个 wasm 实例然后暂停时,它看起来像什么? 为了解释这一点,我们先来看一个在将原生应用程序移植到 wasm 时遇到的常见示例,即事件循环:

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

想象一下,这个函数每requestAnimationFrame调用一次。 它执行分配给它的任务,其中可能包括:渲染、物理、音频和网络获取。 如果我们有一个网络获取事件,那么并且只有这样我们才能最终在fetch的 Promise 上运行await指令。 对于event_loop_iteration的一次调用,我们可能会这样做 0 次,或者 1 次,或者多次。 我们只知道我们是否在执行此 wasm 期间最终这样做 - 不是在之前,特别是在此 wasm 导出的 JS 调用者中。 因此,调用者必须准备好让实例暂停或不暂停。

在纯 JavaScript 中可能会发生类似的情况:

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

foo得到一个 JS 函数bar并用一些数据调用它。 在 JS 中bar可能是一个异步函数,也可能是一个普通函数。 如果它是异步的,它会返回一个 Promise,并且只会在稍后完成执行。 如果正常,则在返回之前执行并返回实际结果。 foo可以假设它知道bar是哪种类型(JS 中没有编写类型,实际上bar甚至可能不是函数!),或者它可以处理这两种类型的功能要完全通用。

现在,通常你知道bar可能是什么函数集! 例如,您可能已经编写foo和可能的bar协调,或者准确记录了期望是什么。 但是我们这里所说的 wasm/JS 交互实际上更类似于事物之间没有如此紧密耦合的情况,实际上你需要处理这两种情况。 如前所述, event_loop_iteration示例要求这样做。 但更一般地说,wasm 通常是您编译的应用程序,而 JS 是通用的“运行时”代码,因此 JS 必须处理所有情况。 当然,JS 可以很容易地做到这一点,例如使用result instanceof Promise来检查结果,或者使用 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");
}

(请注意,如果我们不需要console.log ,那么在此示例中我们将不需要 JS await ,并且只需正常调用 wasm 导出)

综上所述,我们建议暂停 wasm 实例的行为以 JS 情况为模型,该函数可能是异步的,也可能不是异步的,我们可以将其声明为:

  • 当执行await时,wasm 实例会立即退出给调用它的任何人(通常是 JS 调用 wasm 导出,但请参阅后面的注释)。 调用者会收到一个 Promise,它可以用来知道 wasm 的执行何时结束,并在有结果时获得结果。

工具链/库支持

根据我们使用 Asyncify 和相关工具的经验,编写一个小 JS 来处理等待的 wasm 实例很容易(而且很有趣!)。 除了前面提到的选项之外,库还可以执行以下操作之一:

  1. 环绕一个 wasm 实例,使其导出总是返回一个 Promise。 这为外部提供了一个很好的简单接口(但是,它增加了对暂停的 wasm 的快速调用的开销)。 例如,这就是独立的 Asyncify 帮助程序库所做的。
  2. 当实例暂停时写入一些全局状态,并从调用实例的 JS 中检查。 例如,这就是 Emscripten 的 Asyncify 集成所做的。

在这些方法或其他方法之上可以构建更多内容。 我们更愿意将所有这些留给工具链和库,以避免提案和 VM 中的复杂性。

实施和绩效

几个因素应该有助于保持 VM 实现简单:

  1. 暂停/恢复仅在等待时发生,我们静态地知道它们在每个函数中的位置。
  2. 当我们恢复时,我们会从我们离开的地方继续,而且我们只这样做一次。 特别是,我们从不“分叉”执行:这里没有任何东西返回两次,这与 C 的setjmp或允许克隆/分叉的系统中的协程不同。
  3. 如果await的速度比正常调用 JS 的速度慢是可以接受的,因为我们将等待一个 Promise,这至少意味着分配了一个 Promise 并且我们等待事件循环(它具有最小的开销加上可能等待当前正在运行的其他事物)。 也就是说,这里的用例并不要求 VM 实现者想办法让await变得非常快。 与这里的要求相比,我们只希望await更高效,并且特别希望它比 Asyncify 的大量开销要快得多。

鉴于上述情况,一个自然的实现是在我们暂停时复制堆栈。 虽然这有一些开销,但考虑到这里的性能预期,它应该是非常合理的。 如果我们只在暂停时复制堆栈,那么我们可以避免做额外的工作来准备暂停。 也就是说,不应该有额外的一般开销(这与 Asyncify 非常不同!)

请注意,虽然在这里复制堆栈是一种自然的方法,但它并不是一个完全简单的操作,因为复制可能不是简单的 memcpy,这取决于 VM 的内部结构。 例如,如果堆栈包含指向自身的指针,则需要调整这些指针,或者使堆栈可重定位。 或者,可以在恢复堆栈之前将堆栈复制回其原始位置,因为如前所述,它永远不会“分叉”。

另请注意,此提案中的任何内容都不需要复制堆栈。 由于本节前面的 3 点中提到的简化因素,也许某些实现可以做其他事情。 这里的可观察行为相当简单,显式堆栈处理不是其中的一部分。

我们非常有兴趣听到 VM 实现者对本节的反馈!

澄清

这个提议只会暂停 WebAssembly 的执行,然后返回给 wasm 实例的调用者。 它不允许暂停主机(JS 或浏览器)堆栈帧。 await对 wasm 实例进行操作,仅影响其中的堆栈帧。

在发生暂停时调用 WebAssembly 实例是可以的,并且多个暂停/恢复事件可以同时进行。 (注意,如果虚拟机采取了复制栈的方式,那么这并不意味着每次进入模块都必须分配一个新的栈,因为我们只有在真正暂停时才需要复制它。)

与其他提案的联系

例外

Promise 拒绝抛出异常意味着这个提议依赖于 wasm 异常提议。

协程

Andreas Rossberg 的协程提案还涉及暂停和恢复执行。 然而,虽然存在一些概念上的重叠,但我们认为这些提案并不具有竞争力。 两者都很有用,因为它们专注于不同的用例。 特别是,coroutines 提案允许协程在 wasm内部之间切换,而 await 提案允许整个实例等待外部环境。 这两件事的完成方式导致了不同的特征。

具体来说,协程提案以显式方式处理堆栈创建(提供了创建协程、暂停协程等的指令)。 await 提案只讨论暂停和恢复,因此堆栈处理是隐式的。 当您知道您正在创建特定的协程时,显式堆栈处理是合适的,而当您只知道在执行期间需要等待某些东西时,隐式处理是合适的(参见前面的示例event_loop_iteration )。

这两个模型的性能特征可能非常不同。 例如,如果我们在每次运行可能会暂停的代码时创建一个协程(同样,通常我们事先并不知道),这可能会不必要地分配内存。 观察到的await的行为比一般协程可以做的更简单,因此它可能更容易实现。

另一个显着的区别是await是一条指令,它提供了 wasm 模块所需的所有内容,以修复 wasm 与 Web 的同步/异步不匹配(参见第一个.wat示例开始)。 它在 JS 端也很容易使用,它可以提供和/或接收 Promise(虽然添加一些库代码可能很有用,如前所述,它可能非常少)。

从理论上讲,这两个提案可以设计成互补的。 也许await可能是协程提案中的指令之一? 另一种选择是允许await在协程上运行(基本上为 wasm 实例提供了一种等待协程结果的简单方法)。

WASI#276

巧合的是, WASI #276是由@tqchen发布的,就在我们完成撰写本文时。 我们很高兴看到这一点,因为它与我们一样相信协程和异步支持是独立的功能。

我们相信await指令可以帮助实现与那里提出的非常相似的东西(选项 C3),不同之处在于不需要特殊的异步系统调用,而是一些系统调用可以返回waitref然后可以是await -ed。

对于 JavaScript,我们将等待定义为暂停 wasm 实例,这是有道理的,因为我们可以在页面上拥有多个实例以及 JavaScript。 但是,在某些服务器环境中,可能只有主机和单个 wasm 实例,在这种情况下,等待会简单得多,可能实际上是在文件描述符或 GPU 上等待。 或者等待可以暂停整个 wasm VM,但继续运行事件循环。 我们自己在这里没有具体的想法,但是根据那个问题的讨论,这里可能会有一些有趣的可能性,我们很好奇人们的想法!

极端案例:wasm 实例 => wasm 实例 => 等待

在 JS 环境中,当 wasm 实例暂停时,它会立即返回给调用它的人。 我们描述了如果调用者来自 JS 会发生什么,如果调用者是浏览器也会发生同样的事情(例如,如果我们在暂停的 wasm 导出上执行了setTimeout ;但那里没有任何有趣的事情发生,因为返回的 Promise 被忽略)。 但是还有另一种情况,来自 wasm 的调用,即 wasm 实例A直接调用实例B的导出,并且B暂停。 暂停使我们立即退出B并返回Promise

当调用者是 JavaScript 时,作为一种动态语言,这不是什么问题,事实上,期望调用者检查前面讨论的类型是合理的。 当调用者是静态类型的 WebAssembly 时,这很尴尬。 如果我们不在提案中为此做任何事情,那么值将被强制转换,在我们的示例中,从 Promise 到A期望的任何实例(如果i32 ,它将被强制转换为0 )。 相反,我们建议发生错误:

  • 如果一个 wasm 实例调用(直接或使用call_indirect )来自另一个 wasm 实例的函数,并且在另一个实例中运行时执行await ,则RuntimeError异常是从await的位置抛出。

重要的是,这可以在没有开销的情况下完成,除非暂停,也就是说,通过仅在暂停时检查堆栈来保持正常的wasm instance -> wasm instance调用全速。

请注意,确实希望像 wasm 实例这样的东西来调用另一个并且有后者暂停的用户可以这样做,但他们需要在两者之间添加一些 JS。

这里的另一个选项是暂停也传播到调用 wasm,也就是说,所有 wasm 都会一直暂停到 JS,可能跨越多个 wasm 实例。 这有一些优点,比如 wasm 模块边界不再重要,但也有缺点,比如传播不太直观(调用实例的作者可能不期望这种行为),并且在中间添加 JS 可能会改变行为(也可能出乎意料)。 如前所述,要求用户在两者之间使用 JS 似乎风险较小。

另一种选择可能是将一些 wasm 导出标记为异步,而另一些则没有,然后我们可以静态地知道什么是什么,并且不允许不正确的调用; 但是参见前面的event_loop_iteration示例,这是一个常见的情况,无法通过标记导出来解决,并且还有间接调用,因此我们无法避免该问题。

考虑的替代方法

也许我们根本不需要新的await指令,如果当 JS 导入返回 Promise 时 wasm 暂停? 问题是现在当 JS 返回一个不是错误的 Promise 时。 这种向后不兼容的更改意味着 wasm 不能在暂停的情况下不再接收 Promise,但这也可能有用。

我们考虑的另一个选择是以某种方式标记导入以表示“如果它返回一个 Promise,这个导入应该暂停”。 我们考虑了如何在 JS 或 wasm 方面标记它们的各种选项,但没有找到任何感觉正确的方法。 例如,如果我们在 JS 端标记导入,那么当导入到达时,wasm 模块将不知道对导入的调用是否暂停,直到链接步骤。 也就是说,对导入和暂停的调用将“混合在一起”。 似乎最直接的事情就是为此添加一条新指令await ,它明确表示等待。 从理论上讲,这种功能在 Web 之外也可能有用(参见前面的注释),因此为每个人提供指导可能会使事情总体上更加一致。

之前的相关讨论

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

感谢阅读,欢迎反馈!

最有用的评论

我希望在这里进行更多公开讨论,但为了节省时间,我直接联系了一些 VM 实施者,因为到目前为止很少有人在这里参与。 鉴于他们的反馈以及此处的讨论,遗憾的是,我认为我们应该暂停该提案。

与一般的协程或堆栈切换相比,Await 具有更简单的可观察行为,但我与之交谈的 VM 人员同意@rossberg的观点,即最终的 VM 工作可能对两者都相似。 并且至少一些 VM 人员相信无论如何我们都会获得协程或堆栈切换,并且我们可以使用它来支持 await 的用例。 这将意味着在每次调用 wasm 时创建一个新的协程/堆栈(与本提案不同),但至少一些 VM 人员认为这可以做得足够快。

除了 VM 人员缺乏兴趣之外,我们还对@fgmccabe@RossTate的这个提议提出了一些强烈反对,如上所述。 我们在某些事情上存在分歧,但我很欣赏这些观点,以及解释它们的时间。

总而言之,总的来说,尝试在这里前进是浪费每个人的时间。 但是感谢所有参与讨论的人! 希望至少这能激发协同程序/堆栈切换的优先级。

请注意,该提案的 JS 部分将来可能会相关,因为 JS 糖基本上是为了方便 Promise 集成。 我们需要等待堆栈切换或协程,看看这是否可以在此基础上工作。 但我认为不值得让这个问题保持开放,所以关闭。

所有96条评论

优秀的文案! 我喜欢主机控制暂停的想法。 @rossberg的提案还讨论了功能效果系统,我承认我不是它们的专家,但乍一看,它们似乎可以满足相同的非本地控制流需求。

关于:“鉴于上述情况,一个自然的实现是在我们暂停时复制堆栈。” 这将如何用于执行堆栈? 我想大多数 JIT 引擎在 JS 和 wasm 之间共享本机 C 执行堆栈,所以我不确定在这种情况下保存和恢复意味着什么。 这个提议是否意味着需要对 wasm 执行堆栈进行虚拟化? 当 python 尝试做类似的事情时,IIUC 避免像这样使用 C 堆栈非常棘手: https://github.com/stackless-dev/stackless/wiki。

我与@sbc100 有类似的担忧。 复制堆栈本质上是一项相当困难的操作,尤其是在您的 VM 还没有 GC 实现的情况下。

@sbc100

这个提议是否意味着需要对 wasm 执行堆栈进行虚拟化?

由于我不是这方面的专家,因此我必须将其留给 VM 实施者。 而且我不了解与无堆栈python的连接,但也许我不知道什么足以理解这种连接,对不起!

但总的来说:各种协程方法通过在低级别操作堆栈指针来工作。 这些方法在这里可能是一种选择。 我们想指出,即使必须将堆栈作为这种方法的一部分进行复制,在这种情况下这样做也有可接受的开销。

(我们不确定这些方法是否可以在 wasm VM 中工作 - 希望听到实施者的意见,如果是或否,以及是否有更好的选择!)

@lachlansneff

您能否更详细地解释一下 GC 使事情变得更容易的意思? 我不跟。

@kripken GC 通常(但并非总是)具有遍历堆栈的能力,如果您需要重写堆栈上的指针以指向新堆栈,这是必要的。 也许对 JSC 有更多了解的人可以确认或否认这一点。

@lachlansneff

谢谢,现在我明白你在说什么了。

我们建议以如此完整的方式遍历堆栈(一直识别每个本地等)来执行此操作。 (对于其他可能的方法,请参阅我上次评论中关于低级协程实现方法的链接。)

对于提案中“复制堆栈”的术语,我深表歉意 - 根据您和@sbc100的反馈,我发现它不够清楚。 同样,我们不想建议特定的 VM 实现方法。 我们只是想说,如果某些方法中需要复制堆栈,那对速度来说不是问题。

与其提出具体的实施方法,我们希望听到 VM 人员的意见,他们认为如何做到这一点!

我很高兴看到这个提议。 Lucet 拥有yieldresume运算符已有一段时间了,我们精确地使用它们与运行在 Rust 主机环境中的异步代码进行交互。

将这添加到 Lucet 相当简单,因为我们的设计已经承诺为 Wasm 执行维护一个单独的堆栈,但我可以想象它可能会给不这样做的 VM 带来一些实现困难。

这个提案听起来很棒! 我们一直在尝试找到一种在 wasmer-js 上管理异步代码的好方法(因为我们无法在浏览器上下文中访问 VM 内部)。

与其提出具体的实施方法,我们希望听到 VM 人员的意见,他们认为如何做到这一点!

我认为也许使用异步函数的回调策略可能是让事情滚动的最简单的方法,也是一种与语言无关的方式。

似乎.await可以使用wasm-bindgen-futures $ 在 Rust 函数内的JsPromise中调用? 如果没有此处提出的await指令,这将如何工作? 我很抱歉我的无知,我正在寻找在 wasm 中调用 fetch 的解决方案,并且我正在学习 Asyncify,但它表明 Rust 解决方案更简单。 我在这里缺少什么? 有人可以帮我说清楚吗?

我对这个提议感到非常兴奋。 该提案的主要优点是简单,因为我们可以构建与 wasm 的 POV 同步的 API,并且它使移植应用程序变得更加容易,而无需明确考虑回调和 async/await。 它将使我们能够使用单个原生 API 将基于 WASM 和 WebGPU 的机器学习引入原生 wasm vm,并在 Web 和原生上运行。

我认为值得讨论的一件事是可能调用 await 的函数的签名。 假设我们有以下函数

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

对应函数的签名是() => i32 。 根据新提案,对 test 的调用可以返回 i32 或Promise<i32> 。 请注意,要求用户静态声明新签名更难(因为代码移植的成本,并且可能是我们不知道调用 await 的函数内部的间接调用)。

我们是否应该在导出的函数中使用单独的调用模式(例如异步调用)来指示在运行时允许等待?

在术语方面,建议的操作就像操作系统中的屈服操作。 因为它将控制权交给操作系统(在本例中为 wasm VM)以等待系统调用 finsih。

如果我正确理解了这个提议,我认为它大致相当于取消了 JS 中的await只能在async函数中使用的限制。 也就是说,在 wasm 方面waitref可能是externref而不是await指令,您可以有一个导入的函数$await : [externref] -> [] ,而在 JS 方面您可以提供foo(promise) => await promise作为要导入的函数。 在另一个方向上,如果您是想要在async函数之外的 Promise 上await的 JS 代码,您可以将该承诺提供给简单调用await的 wasm 模块在输入上。 这是正确的理解吗?

@RossTate不完全是,AIUI。 wasm 代码可以await一个承诺(称之为promise1 ),但只有 wasm 执行会产生,而不是 JS。 wasm 代码将向 JS 调用者返回一个不同的承诺(称之为promise2 )。 当promise1解析时,wasm 执行将继续。 最后,当该 wasm 代码正常退出时, promise2将使用 wasm 函数的结果进行解析。

@tqchen

我们是否应该在导出的函数中使用单独的调用模式(例如异步调用)来指示在运行时允许等待?

有趣 - 你在哪里看到好处? 正如您所说,在常见的移植情况下,确实无法确定导出是否最终会执行await ,因此充其量只能在有时使用。 这可能会在内部帮助虚拟机吗?

有一个明确的声明可以确保用户清楚地陈述他们的意图,如果用户的意图不是执行异步调用,VM 可能会抛出正确的错误消息。

从用户的 POV 来看,这也使得代码编写更加一致。 例如,即使 test 没有调用 await,用户也可以编写以下代码,并且系统接口会自动返回 Promise.resolve(test())。

await inst.exports_async.test();

从用户的 POV 来看,这也使得代码编写更加一致。 例如,即使 test 没有调用 await,用户也可以编写以下代码,并且系统接口会自动返回 Promise.resolve(test())。

@tqchen请注意,用户已经可以执行此操作,如提案测试中的示例所示。 也就是说,JavaScript 已经以相同的方式支持和处理await运算符中的同步和异步值。

如果建议是强制执行单个静态类型,那么我们相信这可以在 lint 或类型系统级别或 JavaScript 包装器级别完成,而不会在核心 WebAssembly 方面引入复杂性或限制此类包装器的实现者。

啊,谢谢你的纠正,@binji。

在那种情况下,以下大致等价吗? 将WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2")函数添加到 JS API。 假设moduleBytes有多个导入加上一个额外的导入import "name1" "name2" (func (param externref)) 。 然后这个函数用imports给定的值实例化导入,用概念上的await实例化额外的导入。 当从这个模块创建导出的函数时,它们会受到保护,因此当这个await被调用时,它会沿着堆栈向上查找第一个保护,然后将堆栈的内容复制到一个新的 Promise 中,然后立即返回。

那行得通吗? 我的感觉是这个提议可以只通过修改 JS API 来完成,而不需要修改 WebAssembly 本身。 当然,即便如此,它仍然增加了很多有用的功能。

@kripken如何处理start函数? 它会静态禁止await ,还是会以某种方式与 Wasm 实例化交互?

@malbarbo wasm-bindgen-futures允许您在 Rust 中运行async代码。 这意味着您必须以异步方式编写程序:您必须将您的函数标记为async ,并且您需要使用.await 。 但是这个提议允许你在使用async.await的情况下运行异步代码,相反它看起来像一个常规的同步函数调用。

换句话说,您目前不能使用同步 OS API(例如std::fs ),因为 Web 只有异步 API。 但是通过这个提议,您可以使用同步的 OS API:它们将在内部使用 Promises,但它们看起来与 Rust 同步。

即使实施了这个提议, wasm-bindgen-futures仍然存在并且仍然有用,因为它正在处理不同的用例(运行async函数)。 async函数很有用,因为它们可以很容易地并行化。

@RossTate看来您的建议与“考虑的替代方法”中的建议非常相似:

我们考虑的另一个选择是以某种方式标记导入以表示“如果它返回一个 Promise,这个导入应该暂停”。 我们考虑了如何在 JS 或 wasm 方面标记它们的各种选项,但没有找到任何感觉正确的方法。 例如,如果我们在 JS 端标记导入,那么当导入到达时,wasm 模块将不知道对导入的调用是否暂停,直到链接步骤。 也就是说,对导入和暂停的调用将“混合在一起”。 似乎最直接的事情就是为此添加一条新指令,等待,它明确表示等待。 从理论上讲,这种功能在 Web 之外也可能有用(参见前面的注释),因此为每个人提供指导可能会使事情总体上更加一致。

如何处理启动功能? 它会静态地禁止等待,还是会以某种方式与 Wasm 实例化交互?

@Pauan我们没有具体介绍这一点,但我认为没有什么能阻止我们在start中也允许await 。 在这种情况下,当 start 函数完全执行完毕后,从instantiate{Streaming}返回的 Promise 仍然会自然地解析/拒绝,唯一的区别是它会等待await ed Promise。

也就是说,与今天相同的限制适用,现在它对于需要访问例如导出内存的情况不会太有用。

@RReverser这对于同步new WebAssembly.Instance (用于工人)如何工作?

关于开始的有趣点@Pauan

是的,对于同步实例化,这似乎是有风险的——如果允许await ,如果有人在导出暂停时调用它,那就很奇怪了。 禁止await可能是最简单和最安全的。 (也许在异步启动中也是为了保持一致性,似乎没有重要的用例可以阻止?需要更多考虑。)

(用于工人)?

嗯,好点; 我不认为它必须在 Workers 中使用,但由于这个 API 已经存在,也许它可以返回一个 Promise? 我认为这是一种半流行的新兴模式,可以从各种库的构造函数返回 thenables,尽管不确定在标准 API 中执行此操作是否是个好主意。

我同意在start中禁止它(如在陷阱中)是目前最安全的,如果发生变化,我们总是可以在未来以向后兼容的方式改变它。

也许我错过了一些东西,但是没有讨论当 WASM 执行暂停并使用await指令和返回给 JS 的承诺,然后 JS 在不等待承诺的情况下回调到 WASM 时会发生什么。

这是一个有效的用例吗? 如果是,那么它可以允许“主循环”应用程序接收输入事件,而无需手动屈服于浏览器。 相反,他们可以通过等待立即解决的承诺来让步。

取消怎么办? 它没有在 JS 承诺中实现,这会导致一些问题。

@康兹

也许我错过了一些东西,但是没有讨论当 WASM 执行暂停并返回给 JS 的等待指令和承诺时会发生什么,然后 JS 回调到 WASM 而不等待承诺。

这是一个有效的用例吗? 如果是,那么它可以允许“主循环”应用程序接收输入事件,而无需手动屈服于浏览器。 相反,他们可以通过等待立即解决的承诺来让步。

目前的案文可能对此不够清楚。 对于第一段,是的,这是允许的,请参阅“澄清”部分: 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.

对于第二段,不——你不能更早地得到事件,你不能让 JS 比它更早地解决一个 Promise。 让我试着用另一种方式来总结一下:

  • 当 wasm 在 Promise A 上暂停时,它会退出到调用它的任何地方,并返回一个新的 Promise B。
  • 当 Promise A 解决时,Wasm 恢复。 这发生在正常时间,这意味着在 JS 事件循环中一切正常。
  • wasm 恢复完成运行之后,Promise B 才被解决。

所以特别是 Promise B 必须在 Promise A 之后解决。你不能在 JS 得到它之前得到 Promise A 的结果。

换句话说:这个提案的行为可以通过 Asyncify + 一些使用 Promises 的 JS 来填充。

@RReverser ,我认为这些不一样,但首先我认为我们需要澄清一些事情(如果尚未澄清,在这种情况下我很抱歉错过了它)。

JS 可以同时对同一个堆栈上的同一个 wasm 实例进行多次调用。 如果await被实例执行,哪个调用会暂停并返回一个承诺?

对于第二段,不——你不能更早地得到事件,你不能让 JS 比它更早地解决一个 Promise。

抱歉,我认为我的问题不清楚。 目前,C++ 中的“主循环”应用程序使用emscripten_set_main_loop ,以便在每次运行 frame 函数之间,将控制权交还给浏览器,并且可以处理输入或其他事件。

有了这个提议,似乎以下内容应该可以翻译“主循环”应用程序。 (虽然我不太了解 JS 事件循环)

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

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

@Kangz应该可以,是的(除非您的 setTimeout 代码中的参数顺序有一个小问题,而且可以简化):

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

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

JS 可以同时对同一个堆栈上的同一个 wasm 实例进行多次调用。 如果 await 被实例执行,哪个调用会被暂停并返回一个 Promise?

最里面的一个。 如果希望这样做,JS 包装器的工作就是协调其余部分。

@Kangz对不起,在那之前我误解了你。 是的,正如@RReverser所说,这应该可以工作,这是此处预期用例的一个很好的例子!

正如您所说,它可以使用 Asyncify 进行填充,实际上它等同于今天使用 Asyncify 的相同代码,方法是将__builtin_await替换为对emscripten_sleep(0)的调用(它执行setTimeout(0) ) .

感谢@RReverser的澄清。 我认为将描述重新表述为对实例的(最近)调用暂停而不是实例本身会有所帮助。

在这种情况下,这听起来几乎等同于在 JS 中添加以下两个原始函数: promise-on-await(f)await-for-promise(p) 。 前者调用f()但是,如果在执行f()期间调用await-for-promise(p) ,则返回一个新的 Promise,该 Promise 在p解决后恢复执行并在执行完成后自行解决(或再次调用await-for-promise )。 如果在多个promise-on-await的上下文中调用了await-for-promise ,那么最近的一个会返回一个 Promise。 如果在任何promise-on-await之外调用await-for-promise $ ,则会发生一些不好的事情(就像实例的start代码执行await一样)。

那有意义吗?

@RossTate非常接近,是的,并且抓住了总体思路。 (但正如你所说,只是几乎等价,因为它不能用于填充它,并且它缺少特定的 wasm/JS 边界处理。)

感谢您提出改写该文本的建议。 我在此处的讨论中保留了此类注释的列表。 (我不确定是否值得将它们应用于第一篇文章,因为随着时间的推移不改变它似乎不那么令人困惑?)

@RossTate有趣……我喜欢这个! 它使调用的异步性质显式(任何潜在的异步调用都需要promise-on-await ),并且不需要对 Wasm 进行任何更改。 如果你从中间移除 Wasm,它也有(一些)意义——如果promise-on-await await-for-promise ,那么它会返回一个Promise

@kripken您能否详细说明为什么会有所不同? 我不太明白为什么 Wasm/JS 边界在这里很重要。

@binji我只是说JS中的这些功能不会让wasm做类似的事情。 将它们称为从 wasm 进口是行不通的。 我们仍然需要一种方法让 wasm 以可恢复的方式退出边界等,不是吗?

@kripken对,我想那时await-for-promise导入必须像 Wasm 内在函数一样运行。

我的想法是,这样的模块不会向 wasm 添加await指令,而是导入await-for-promise并调用它。 同样,JS 代码不会更改导出的函数,而是在promise-on-await中调用它们。 这意味着 JS 原语将处理所有堆栈工作,包括 WebAssembly 堆栈。 它也会更灵活,例如,如果你愿意,你可以给模块一个 JS 回调,然后可以回调到模块中并让外部调用暂停而不是内部子句——这完全取决于 JS 代码是否选择是否将呼叫包装在promise-on-await中。 我认为您不需要将任何东西更改为 wasm 本身。

我很想听听@syg对这些潜在的 JS 原语的看法。

哦,好的,抱歉 - 我把你的评论@RossTate理解为“确保我理解,让我这样改写它,并告诉我它的形状是否正确”,而不是一个具体的建议。

考虑一下,您的想法不仅要暂停 JS 框架,还要暂停 wasm,而且还有主机/浏览器框架。 (当前的提议通过只在调用它的边界上处理 wasm 来避免这种情况。)这里有一个例子:

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

如果在浏览器代码中实现了forEach ,则意味着暂停浏览器框架。 同样重要的是,在这样的循环中间暂停,然后再恢复,将是 JS 可以做的一种新的力量,你的想法也允许它用于一个正常的循环:

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

所有这些都可能与async JS 函数有奇怪的规范交互。 这些似乎都是与浏览器和 JS 人员进行的大型讨论。

而且,这只避免了将awaitwaitref添加到核心 wasm 规范中,但这些只是微小的添加 - 因为它们在核心规范中没有任何作用。 目前的提案在 JS 端已经有 99% 的复杂度。 并且 IIUC 你的提议在 JS 方面用更大的添加来权衡 wasm 规范的那个小添加 - 所以它使 Web 平台作为一个整体更加复杂,而且没有必要,因为这一切都是为了 wasm。 另外,在核心 wasm 规范中定义await实际上有一个好处,它可能在 Web 之外有用。

也许我错过了您的建议中的某些内容,如果有,请道歉。 总的来说,我很好奇你试图避免添加核心 wasm 规范的动机是什么?

我认为这些原语对 js 没有多大意义,而且我认为比浏览器中的更多的 wasm 实现可以从中受益。 我仍然很好奇为什么可恢复的异常(大致效果)不能满足这个用例。

我的评论是两者的结合。 在高层次上,我试图弄清楚是否有办法将提案重新表述为纯粹对 JS API 的丰富(以及其他主机如何与 wasm 模块交互)。 该练习有助于评估是否真的需要更改 wasm,并有助于确定该提案是否真的在秘密地向 JS 中添加新的原语,而 JS 人可能会或可能不会赞成。 也就是说,如果仅使用导入的await : func (param externref) (result externref)不可能的,那么这很可能是在向 JS 添加新功能。

至于对 wasm 的更改的简单性,还有很多事情需要考虑,比如如何处理模块到模块的调用,当导出的函数返回包含指向可以执行的函数的指针的 GC 值时怎么办await通话结束后,依此类推。

回到练习,正如您所指出的,有充分的理由只捕获 wasm 堆栈。 这让我回到了我之前的建议,尽管用一些新的观点稍微修改了一下。 将WebAssembly.instantiateAsync(moduleBytes, imports, "name1", "name2")函数添加到 JS API。 假设moduleBytes有多个导入加上一个额外的导入import "name1" "name2" (func (param externref) (result externref)) 。 然后instantiateAsync $ 简单地用imports给出的值实例化moduleBytes的其他导入,并用概念上的await-for-promise实例化附加导入。 当从这个实例创建导出的函数时,它们会受到保护(概念上是由promise-on-await ),因此当调用这个await-for-promise时,它会沿着堆栈查找第一个保护,然后复制堆栈进入一个新的 Promise,然后立即返回。 现在我们拥有与我上面提到的相同的原语,但它们不再是一流的,并且这种受限模式确保只有 wasm 堆栈会被捕获。 同时,无需更改 WebAssembly 即可支持该模式。

想法?

@devsnek

我仍然很好奇为什么可恢复的异常(大致效果)不能满足这个用例。

当然,它们是这个领域的一个选择。

我从@rossberg最后一次演讲中了解到,他最初想走这条路,但后来改变了方向,采用了协程方法。 请参阅标题为“问题”的幻灯片。 在那张幻灯片之后,将描述协程,这是该领域的另一个选择。 所以也许你的问题更多是@rossberg可以澄清的?

这个提议的重点是解决同步/异步问题,它不需要像可恢复异常或协程那样多的能量。 那些专注于 wasm 模块内部的交互,而我们专注于 wasm 模块与外部之间的交互(因为这是发生同步/异步问题的地方)。 这就是为什么我们只需要核心 wasm 规范中的一条新指令,并且该提案中的几乎所有逻辑都在 wasm JS 规范中。 这意味着你可以等待这样的 Promise:

call $get_promise
await
;; use it!

wasm 中的简单性对其本身很有用,但也意味着 VM 非常清楚正在发生的事情,这也可能有好处。

@RossTate

也就是说,如果仅使用导入的 await : func (param externref) (result externref) 是不可能的,那么这很可能是在向 JS 添加新功能。

我不遵循这个推论,对不起。 但这对我来说似乎是迂回的。 如果你认为这个提议为 JS 添加了新功能,为什么不直接展示呢? (我坚信它不会,但如果你发现我们犯了错误,我很好奇!)

至于对 wasm 的更改的简单性,还有很多事情需要考虑,比如如何处理模块到模块的调用

核心 wasm 规范是否说明了关于模块到模块调用的任何内容? 我不记得这样做了,现在浏览相关部分我看不到。 但也许我错过了什么?

我的信念是,核心 wasm 规范添加基本上是列出await ,说它是为了“等待某事”,就是这样。 这就是为什么我在提案中写了That's it for the core wasm spec! 。 如果我错了,请在核心 wasm 规范中向我展示我们需要添加更多内容的地方。

让我们推测一下,有一天核心 wasm 规范将有一条新指令来创建一个 wasm 模块并在其上调用一个方法。 在那种情况下,我想我们会说await只是陷阱,因为它的目的是等待外部,在主机上的某些东西。

这让我回到了我之前的建议,虽然稍微修改了一些新的观点[新想法]

这个想法在功能上与提案中Alternative approaches considered中的第二段不同吗? 这样的事情是可以做到的,但我们解释了为什么我们认为它不太好。

@kripken明白了。 需要明确的是,我认为await以一种非常实用和优雅的方式解决了所呈现的用例。 我只是也有点希望我们可以利用这种势头来解决其他用例,以及通过稍微扩大设计。

我认为@RossTate的建议确实听起来很像“考虑的替代方法”中提到的内容。 所以我认为我们应该更详细地讨论为什么该方法被驳回。 我认为我们都同意,如果我们可以使 JS 端可行,那么不涉及 wasm 规范更改的解决方案将是可取的。 我试图了解您在该部分中列出的缺点,以及为什么它们使仅 JS 的解决方案如此不可接受。

我认为我们都同意不涉及 wasm 规范更改的解决方案会更好

不! 请参阅此处讨论的非 Web 用例。 如果在 wasm 规范中没有await ,我们最终会让每个平台都做一些特别的事情:JS 环境做一些导入的事情,其他地方创建标记为“同步”的新 API,等等。wasm 生态系统会不太一致,将wasm从Web移动到其他地方会更加困难,等等。

但是,是的,我们应该使核心 wasm 规范部分尽可能简单。 我认为这样做? 99% 的逻辑都在 JS 方面(但@RossTate似乎不同意,我们仍在努力解决这个问题——我在最后的回复中提出了具体问题,希望能推动事情发展)。

我的信念是,核心 wasm 规范添加基本上是列出await ,说它是为了“等待某事”,就是这样。

除非这些语义可以更精确地形式化,否则这会在规范中引入歧义或实现定义的行为。 到目前为止,我们已经避免了这种情况(在 SIMD 的情况下成本很高),所以这绝对是我希望看到的。 我不认为提案本身必须改变以使其更正式,但“等待某事”应该用规范已经使用的精确术语重新措辞。

核心 wasm 规范是否说明了关于模块到模块调用的任何内容?

一个实例的导入可以用另一个实例的导出来实例化。 根据我对 JS API(以及 wasm 的组合性原则)的理解,对这种导入的调用在概念上是对另一个实例导出的任何函数的直接调用。 对于在两个实例之间传递的函数值(如funcref )的(间接)调用也是如此。

让我们推测一下,有一天核心 wasm 规范将有一条新指令来创建一个 wasm 模块并在其上调用一个方法。 在这种情况下,我想我们会说 await 只是陷阱,因为它的目的是等待外部的东西,在主机上。

根据面对面会议上讨论的模块组成原则,它不应该陷入困境。 就好像只有一个(组合的)模块实例并且它执行await 。 也就是说, await会将堆栈打包到最近的 JS 堆栈帧。

请注意,这意味着如果f是某个 wasm 实例的导出一元函数的值,那么实例化参数对象{"some" : {"import" : f}}在语义上将与{"some" : {"import" : (x) => f(x)}}不同,因为调用对前者的调用将留在 wasm 堆栈中,而对后者的调用将进入 JS 堆栈,即使只是勉强。 到目前为止,这些实例化参数对象将被认为是等效的。 我可以从代码迁移/语言互操作的角度探讨为什么这很有用,但目前这是一个题外话。

这个想法在功能上与提案中考虑的替代方法中的第二段不同吗? 这样的事情是可以做到的,但我们解释了为什么我们认为它不太好。

抱歉,我把那个替代方案读成不同的意思,但现在这并不重要,只是为了解释我的困惑。 看来你的意思和我的建议一样,在这种情况下,值得讨论利弊。

这个提议在 wasm 方面如此轻松的事实是因为await指令在语义上似乎与对导入函数的调用相同。 当然,正如您所指出的,约定很重要! 但是await并不是唯一适用的功能。 大多数导入函数也是如此。 在await的情况下,我的感觉是可以通过让具有此功能的模块具有import "control" "await" (func (param externref) (result externref))子句来解决对约定的关注,并且让支持此功能的环境始终实例化该导入使用适当的回调。

这似乎提供了一个解决方案,通过不更改 wasm 来节省大量工作,同时仍提供您正在寻找的跨平台可移植性。 但我仍在努力了解提案的细微差别,到目前为止我已经错过了很多!

这个提议在 wasm 方面如此轻松的事实是因为 await 指令在语义上似乎与对导入函数的调用相同。

FWIW 这是这个提议最初开始的地方,但是使用这样的内在函数对 VM 来说似乎更不透明并且通常不鼓励(我认为@binji建议在最初的讨论中远离它)。

例如,根据你的论点,类似memory.growatomic.wait的东西也可以相应地作为import "control" "memory_grow"import "control" "atomic_wait"来完成,但它们并不像他们那样'不提供与实际指令相同级别的互操作和静态分析机会(在 VM 和工具端)。

您可能会争辩说memory.grow作为指令对于不导出内存的情况仍然有用,但atomic.wait绝对可以在内核之外实现。 事实上,它与await非常相似,除了暂停/恢复发生的级别以及await作为函数需要比atomic.wait更多的魔法。因为它需要能够与 VM 堆栈交互,而不仅仅是阻塞当前线程,直到值更改。

@tlively

“等待某事”应该用规范已经使用的精确术语重新措辞。

肯定的,是的。 如果有帮助,我现在可以建议一些更具体的文本:

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.

顺便说一句,我在写这篇文章时遇到的另一个比较是加载和存储的对齐提示。 Wasm 支持未对齐的加载和存储,因此提示不会导致 wasm 模块可观察到的不同行为(即使提示错误),但是对于主机,他们建议在某些平台上使用非常不同的实现(这可能更有效)。 所以这是一个不同指令的例子,没有内部可观察的不同语义,正如规范所说: The alignment in load and store instructions does not affect the semantics

@RossTate

根据面对面会议上讨论的模块组成原则,它不应该陷入困境。 就好像只有一个(组合的)模块实例并执行等待。 也就是说,await 会将堆栈打包到最近的 JS 堆栈帧。

听起来不错,很高兴知道,谢谢,我错过了那部分。

我认为这向我解释了我们的部分误解。 Module => 模块调用不在 wasm 规范 atm 中,这是我之前的观点。 但听起来你正在考虑未来可能出现的规范。 在任何情况下,这看起来都不是问题,因为组合性确切地确定了 await 在那种情况下应该如何表现(这不是我之前建议的!但更有意义)。

核心 wasm 规范是否说明了关于模块到模块调用的任何内容? 我不记得这样做了,现在浏览相关部分我看不到。 但也许我错过了什么?

是的,核心 wasm 规范区分了从其他 wasm 模块导入的函数和宿主函数(第 4.2.6 节)。 函数调用的语义(第 4.4.7 节)不依赖于定义函数的模块,特别是当前指定的跨模块函数调用与相同模块函数调用的行为相同。

如果跨模块调用下的await被定义为陷阱,则这将需要指定向上遍历调用堆栈以检查在主机调用创建的最后一个虚拟帧之前是否存在跨模块调用(第 4.5.5 节)。 这将是规范中的一个不幸的并发症。 但我同意 Ross 的观点,即跨模块调用陷阱将违反组合性,因此我更喜欢将整个堆栈冻结回主机的最后一次调用的语义。 最简单的规范方法是使await类似于主机函数调用(第 4.4.7.3 节),正如你所说的,@kripken。 但是主机函数调用是完全不确定的,因此从核心规范的角度来看,指令的更好名称可能是undefined 。 在这一点上,我实际上开始更喜欢将始终由 Web 平台提供的内在导入(以及用于可移植性的 WASI),因为核心规范本身并不能从 IMO 的undefined指令中受益。

从语义上讲,返回waitref加上await的主机环境调用只是一个阻塞调用,对吗?

这对没有像浏览器那样的异步环境并且可以原生支持阻塞调用的非 Web 嵌入有什么价值?

@RReverser ,我明白你对内在函数的看法。 在决定何时应该通过未解释的函数与指令来定义操作时,涉及到一个判断调用。 我认为这个判断的一个因素是考虑它如何与其他指令相互作用。 memory.grow影响其他内存指令的行为。 我没有机会仔细阅读 Threads 提案,但我想atomic.wait会影响或受其他同步指令的行为影响。 然后必须更新规范以正式化这些交互。

但是await本身就没有任何与其他指令的交互。 唯一的交互是与主机的交互,这就是为什么我的直觉是这个建议应该通过导入的主机函数来完成。

我认为atomic.wait和这个提议的await之间的一个很大区别是模块不能用atomic.wait重新输入。 代理完全暂停。

@克里普肯

我从@rossberg的最后一次演讲中了解到,他最初想走这条路,但后来改变了方向,采用了协程方法。 请参阅标题为“问题”的幻灯片。 在那张幻灯片之后,将描述协程,这是该领域的另一个选择。 所以也许你的问题更多是@rossberg可以澄清的?

是的,所以协程式分解可以被认为是对先前可恢复异常设计的概括。 它仍然具有可恢复事件/异常的相同概念,但try指令被分解为更小的原语——这使得语义更简单,成本模型更明确。 它也更具表现力。

目的仍然是这可以表达所有相关的控制抽象,而异步是激励用例之一。 为了与 JS 异步进行互操作,JS API 可能会提供一个预定义的await事件(携带一个 JS 承诺作为外部引用),Wasm 模块可以导入该事件并暂停throw 。 当然,还有很多细节需要充实,但原则上应该是可以的。

至于目前的提案,我仍在努力解决它。 :)

特别是,它似乎允许在任何旧的 Wasm 函数中使用await ,我读对了吗? 如果是这样,那与 JS 非常不同,后者只允许在异步函数中使用await 。 这是一个非常重要的约束,因为它使引擎能够通过单个(异步)函数的 _local_ 转换来编译await

如果没有这个限制,引擎要么需要执行 _global_ 程序转换(就像 Asyncify 所做的那样),其中每次调用都可能变得更加昂贵(您通常无法知道某些调用是否可能会等待)。 或者,等效地,引擎需要能够创建多个堆栈并在它们之间切换!

现在,这正是协程/效果处理程序的想法试图引入 Wasm 的特性。 但显然,它是对平台及其执行模型的一个非常重要的补充,JS 已经非常小心地避免了它的控制抽象(例如异步和生成器)的复杂性。

@罗斯伯格

特别是,它似乎允许在任何旧的 Wasm 函数中等待,我读对了吗? 如果是这样,那与 JS 非常不同,后者只允许在异步函数中等待。

是的,这里的模型非常不同。 JS await 是按功能来的,而这个提案是对整个 wasm 实例进行 await(因为目标是解决 JS 和 wasm 之间的同步/异步不匹配,也就是 JS 和 wasm 之间)。 JS await 也是用于手写代码,而这是为了启用编译代码的移植。

这是一个非常重要的约束,因为它使引擎能够通过单个(异步)函数的本地转换来编译等待! 如果没有这个限制,引擎要么需要执行全局程序转换(就像 Asyncify 所做的那样),其中每个调用都可能变得更加昂贵(您通常无法知道某些调用是否可能到达等待状态)。 或者,等效地,引擎需要能够创建多个堆栈并在它们之间切换!

这里绝对不打算进行全局程序转换! 抱歉,如果不清楚。

正如提案中提到的,在堆栈之间切换是一种可能的实现选项,但请注意,它与协程样式的堆栈切换不同:

  • 只有整个 wasm 实例可以暂停。 这不适用于模块内部的堆栈切换。 (特别是,这就是为什么这个提案不能添加到核心 wasm 规范并且完全在 wasm JS 方面;到目前为止,有些人更喜欢这样,我认为任何一种方式都可以工作。)
  • 协程显式声明堆栈,await 没有。
  • 等待堆栈只能恢复一次,不会多次分叉/返回(不确定您的提案中是否会包含它?)。
  • 这里的性能模型非常不同。 await 将等待 JS 中的 Promise,它已经具有最小的开销和延迟。 因此,当我们真正暂停时,如果实现有一些开销是可以的,而且我们可能比协程更关心。

鉴于这些因素,并且该提案的可观察行为是整个 wasm 实例暂停,可能有多种方式来实现它。 例如,在运行单个 wasm 实例的虚拟机中脱离 Web,它实际上可以只运行其事件循环,直到需要恢复 wasm 为止。 在 Web 上,一种实现方法可能是:当 await 发生时,复制整个 wasm 堆栈,从当前位置到我们调用到 wasm 的位置; 把它放在一边; 要恢复,请将其复制回去,然后从那里继续。 可能还有其他方法或变体(有些可能没有复制,但同样,避免复制开销在这里实际上并不重要!)。

很抱歉这篇长文,以及提案文本本身的一些重复,但我希望这有助于澄清你提到的一些观点?

我认为在实施方面有很多要讨论的地方。 到目前为止, @acfoltzer关于 Lucet 的评论令人鼓舞!

只是为了澄清@kripken最近评论中的一些措辞,暂停的并不是整个 wasm 实例。 它只是从主机框架到堆栈上的 wasm 的最新调用被暂停,然后主机框架返回相应的承诺(或主机的适当模拟)。 有关先前的相关说明,请参见此处

嗯,我不明白这有什么不同。 当您在 Wasm 深处的某个地方等待时,您需要捕获至少从主机条目到该点的所有调用堆栈。 您可以根据需要保持该暂停(即,该堆栈段)活动,同时从上面进行其他调用或创建更多暂停。 你可以从其他地方恢复(我想?)。 这不需要分隔延续的所有实现机制吗? 只是提示是在 Wasm 输入时设置的,而不是由单独的构造设置的。

@罗斯伯格

在某些虚拟机上可能是这样,是的。 如果 await 和 coroutines 最终需要完全相同的 VM 工作,那么至少不需要额外的工作。 在这种情况下,await 提案的好处将是方便的 JS 集成。

我认为如果您不允许重新输入模块,您可以在不进行整个程序转换的情况下获得方便的 JS 集成。

我认为如果您不允许重新输入模块,您可以在不进行整个程序转换的情况下获得方便的 JS 集成。

这听起来是一种更简单的方法来完成它,但这需要阻止调用堆栈中访问的任何模块(或作为第一步,所有 WebAssembly 模块)。

这听起来是一种更简单的方法来完成它,但这需要阻止调用堆栈中访问的任何模块(或作为第一步,所有 WebAssembly 模块)。

正确,就像atomic.wait

@taralx

我认为如果您不允许重新输入模块,您可以在不进行整个程序转换的情况下获得方便的 JS 集成。

一方面,重新进入可能很有用,例如,游戏引擎可能会下载一个文件,并且不希望 UI 在这样做时完全暂停(今天 Asyncify 允许这样做)。 但另一方面,可能不允许重新进入,但应用程序可以为此创建同一模块的多个实例(都导入相同的内存、可变全局变量等?),因此重新进入将是一个调用到另一个实例。 我认为我们可以在工具链中实现这一点(对一次活跃的重新进入的数量有一个有效的限制 - 等于实例的数量 - 这似乎很好)。

因此,如果您的简化对虚拟机有所帮助,那绝对值得考虑!

(请注意,尽管如前所述,我认为我们不需要在此处进行整个程序转换以及正在讨论的任何选项。您只需要在 Asyncify 所处的糟糕情况下进行,您可以在工具链级别。对于等待,在与@rossberg讨论的最坏情况下,您可以执行协程提案在内部执行的操作。但是,如果它使事情变得比这更简单,您的想法可能会非常有趣!)

一方面,重新进入可能很有用,例如,游戏引擎可能会下载一个文件,并且不希望 UI 在这样做时完全暂停(今天 Asyncify 允许这样做)。

我不确定这是一个声音功能。 在我看来,这会在应用程序中引入_unexpected concurrency_。 在渲染时加载资产的本机应用程序将在内部使用 2 个线程,每个线程将映射到 WebWorker + SharedArrayBuffer。 如果应用程序使用线程,那么它也可以使用来自 WebWorkers 的同步 Web 原语(至少在某些情况下是允许的)。 否则,始终可以使用 Atomics.wait (例如)将主线程中的异步操作映射到工作线程中的阻塞操作。

我想知道整个用例是否还没有通过多线程解决。 通过在 worker 中使用阻塞原语,整个堆栈(JS/Wasm/浏览器原生)被保留,这似乎更简单和健壮。

通过在 worker 中使用阻塞原语,整个堆栈(JS/Wasm/浏览器原生)被保留,这似乎更简单和健壮。

这实际上是我尝试过的独立 Asyncify JS 包装器的另一种替代实现,但是,虽然它解决了代码大小问题,但性能开销甚至比当前使用 Wasm 转换的 Asyncify 高得多。

@alexp-sssup

在我看来,这会在应用程序中引入意外的并发性。

当然,是的 - 它需要非常小心地完成,并且可能会破坏事情。 我们在使用 Asyncify 方面的经验有好有坏(例如,一个有效的用例:在 JS 中下载一个文件,JS 调用 wasm 以分配一些空间来复制它,然后再恢复)。 但无论如何,无论哪种方式,重新进入都不是该提案的关键部分。

为了补充@RReverser所说的内容,线程的另一个问题是对它们的支持不是也不会是普遍的。 但是 await 可能无处不在。

在其他引入了 async/await 的语言中,重新进入绝对是关键。 其他事件可能会在一个(a)等待时发生,这就是它的全部意义。 在我看来,重新进入非常重要。

此外,当一个模块对外部函数进行任何调用时,它是否必须假设它可以通过其任何导出重新进入(在上面的示例中,即使没有任何等待,任何调用和外部函数是免费的(没有双关语)来调用 malloc)。

应用程序可以为此创建同一模块的多个实例(都导入相同的内存、可变全局变量等?),因此重新进入将是对另一个实例的调用

仅用于模块的共享内存。 必须重新实例化其他存储器,这对于避免一个操作踩踏另一个操作进行中的更改非常重要。

我注意到它的不可重入版本在任何带有线程支持的嵌入上都是可填充的,以防有人想玩它并看看它有多么有用。

我注意到它的不可重入版本在任何带有线程支持的嵌入上都是可填充的,以防有人想玩它并看看它有多么有用。

如上所述,这是我们已经玩过的东西,但被丢弃了,因为它带来的性能比当前解决方案更差,没有得到普遍支持,而且很难分享WebAssembly.GlobalWebAssembly.Table主线程没有额外的黑客攻击,使其成为透明 polyfill 的糟糕选择。

当前重写 Wasm 模块的解决方案不会遇到这些问题,而是会产生巨大的文件大小成本。

因此,这些对于大型现实世界的应用程序都不是很好,这促使我们研究对这里描述的异步集成的原生支持。

性能更差

你有某种基准吗?

是的,我可以在周二(或者,更有可能是周三)回来工作时分享它,或者很容易创建一个只调用自己清空异步 JS 函数的函数。

谢谢。 我可以创建一个微基准,但它不会很有指导意义。

哦,是的,我的也是一个微基准,因为我们只对开销比较感兴趣。

微基准的问题是我们不知道实际应用程序可以接受多少延迟。 如果需要额外的 1 毫秒,例如,如果应用程序仅以 1/s 的速率执行等待操作,那真的有问题吗?

我认为关注基于原子的方法的速度可能会分散注意力。 如前所述,原子不会也不会在任何地方工作(由于 COOP/COEP),而且只有工作人员可以使用原子方法,因为主线程不能阻塞。 这是一个好主意,但对于一个通用的解决方案,我们需要像 Await 这样的东西。

我不建议将其作为长期解决方案。 我建议使用它的 polyfill 可用于查看不可重入解决方案是否适用于人们。

@taralx哦,好的,现在我明白了,谢谢。

@taralx

我认为如果您不允许重新输入模块,您可以在不进行整个程序转换的情况下获得方便的 JS 集成。

那会很糟糕。 这意味着合并多个模块可能会破坏它们的行为。 那将是模块化的对立面。

作为一般设计原则,操作行为不应该依赖于模块边界(除了简单的作用域)。 模块只是 Wasm 中的一种分组和范围机制,您希望保持重新组合内容(链接/合并/拆分模块)的能力,而不会改变程序的行为。

@rossberg :这可以概括为阻止对任何 Wasm 模块的访问,如前所述。 但这可能太局限了。

那会很糟糕。 这意味着合并多个模块可能会破坏它们的行为。 那将是模块化的对立面。

这就是我对 polyfilling 论点的看法 - atomic.wait不会破坏模块化,所以这也不应该。

@taralxatomic.wait引用特定内存中的特定位置。 await会阻塞使用哪个内存和位置,以及如何控制哪些模块共享该内存?

@rossberg你能详细说明你认为这会破坏的场景吗? 我怀疑我们对不可重入版本的工作方式有不同的想法。

@taralx ,考虑加载两个模块 A 和 B,每个模块都提供一些导出功能,比​​如A.fB.g 。 两者都可能在调用时执行await 。 两段客户端代码分别被传递给这些函数中的一个,并且它们独立地调用它们。 它们不会互相干扰或阻挡。 然后有人将 A 和 B 合并或重构为 C,而不更改任何代码。 突然之间,两段客户端代码可能会意外地开始相互阻塞。 通过隐藏的共享状态在远处进行幽灵般的动作。

这就说得通了。 但是允许重新进入会在不期望的模块中冒并发风险,因此无论哪种方式,它都是一种令人毛骨悚然的动作。

但是模块已经可以重新输入了,不是吗? 每当模块调用导入时,外部代码可以重新进入模块,这可能会在返回之前更改全局状态。 我看不出在建议的等待期间重新进入比调用导入的函数更令人毛骨悚然或并发。 也许我错过了什么?

(已编辑)

嗯,是的。 好的,所以一个导入的函数可以重新进入模块。 我显然需要更加努力地考虑这一点。

当代码在运行时,它调用了一个函数,有两种可能:它知道该函数不会调用随机的东西,或者该函数可能会调用随机的东西。 在后一种情况下,重新进入总是可能的。 同样的规则适用于await

(编辑了我上面的评论)

感谢大家到目前为止的讨论!

总而言之,这听起来似乎有普遍的兴趣,但还有很大的悬而未决的问题,比如这应该是 JS 方面的 100% 还是只有 99% - 听起来前者会消除一些人的主要担忧,那就是对于 Web 案例来说没问题,所以这可能没问题。 另一个悬而未决的大问题是,在我们需要更多信息的虚拟机中这样做有多可行。

我将在 2 周后的下一次 CG 会议上提出一个议程项目,以讨论该提案并在第 1 阶段进行审议,这意味着打开一个 repo 并在单独的问题中更详细地讨论未解决的问题。 (我相信这是正确的过程,但如果我错了,请纠正我。)

仅供参考
我们将以类似的方式组合一个完整的堆栈切换提案
大体时间。 我觉得这可能会使您的特殊情况变体没有实际意义-
你怎么看?
弗朗西斯

2020 年 5 月 28 日星期四下午 3:51 Alon Zakai [email protected]写道:

感谢大家到目前为止的讨论!

总而言之,听起来这里有普遍的兴趣,但也有
大的开放性问题,比如这应该是 100% 在 JS 方面还是只是
99% - 听起来前者会消除一些人的主要担忧
有,这对于 Web 案例来说很好,所以这可能没问题。
另一个悬而未决的大问题是,这在 VM 中的可行性如何?
我们需要更多信息。

我会为两周后的下一次 CG 会议建议一个议程项目来讨论
这个提议并在第一阶段考虑它,这意味着打开一个回购
并在那里更详细地讨论单独问题中的未解决问题。
(我相信这是正确的过程,但如果我错了,请纠正我。)


您收到此消息是因为您订阅了此线程。
直接回复此邮件,在 GitHub 上查看
https://github.com/WebAssembly/design/issues/1345#issuecomment-635649331
或退订
https://github.com/notifications/unsubscribe-auth/AAQAXUCLZ4CJVQYEUBK23BLRT3TFLANCNFSM4NEJW2PQ
.

>

弗朗西斯·麦凯布
瑞典语

@fgmccabe

我们应该肯定地讨论这个问题。

不过总的来说,除非您的提案侧重于 JS 方面,否则我猜它不会使这一点变得毫无意义(JS 方面是 99%-100%)。

现在关于实施细节的讨论已经结束,我想再次提出我之前表达的更高级别的关注,但为了一次讨论一个而放弃。

一个程序由许多组件组成。 从软件工程的角度来看,将组件拆分为多个部分或将组件合并在一起不会显着改变程序的行为是很重要的。 这就是在上次面对面的 CG 会议上讨论的模块组合原则背后的原因,它隐含在许多语言的设计中。

对于 Web 程序,现在使用 WebAssembly,这些不同的组件甚至可以用不同的语言编写:JS 或 wasm。 事实上,许多组件也可以用任何一种语言编写。 我将这些称为“矛盾”组件。 现在,大多数矛盾的组件都是用 JS 编写的,但我想我们都希望越来越多的组件被重写为 wasm。 为了促进这种“代码迁移”,我们应该尽量确保以这种方式重写组件不会改变它与环境交互的方式。 作为一个玩具示例,特定的“应用”程序组件(f, x) => f(x)是用 JS 还是用 wasm 编写的,都不应该影响整个程序的行为。 这是一个代码迁移原则。

不幸的是,该提案的所有变体似乎都违反了模块组合程序或代码迁移原则。 当await捕获堆栈直到当前 wasm 模块最近进入的位置时,前者被违反,因为这个边界随着模块被拆分或组合在一起而改变。 当await将堆栈捕获到最近进入 wasm 的位置时,后者就被违反了,因为这个边界随着代码从 JS 迁移到 wasm 而改变(因此迁移像(f, x) => f(x)这样简单的东西JS 到 wasm 可以显着改变整个程序的行为)。

我不认为这些违规行为是由于该提案的设计选择不当造成的。 相反,问题似乎在于,这个提议试图避免间接地让 JS 变得更强大,而这个目标是迫使它强加违反这些原则的人为边界。 我完全理解这个目标,但我怀疑这个问题会越来越多地出现:以尊重这些原则的方式向 WebAssembly 添加功能通常需要间接向 JS 添加功能,因为 JS 是嵌入语言。 我的偏好是直接解决这个问题(我真的不知道如何解决)。 如果不是这样,那么我的次要偏好是仅在 JS API 中进行此更改,因为 JS 是这里的限制因素,而不是向 WebAssembly 添加指令,wasm 没有解释。

我不认为这些违规行为是由于该提案的设计选择不当造成的。 相反,问题似乎是这个提议试图避免间接地让 JS 变得更强大

这很重要,但这不是这里设计的主要原因。

这种设计的主要原因是,虽然我完全同意组合原则对 wasm 有意义,但我们在 Web 上遇到的根本问题是实际上 JS 和 wasm 在实践中并不等同。 我们有异步的手写 JS 和同步的移植 wasm。 换句话说,它们之间的界限实际上正是我们试图解决的问题。 总的来说,我不确定我是否同意组合原则应该应用于 wasm 和 JS(但也许应该,可能是一个有趣的辩论)。

我希望在这里进行更多公开讨论,但为了节省时间,我直接联系了一些 VM 实施者,因为到目前为止很少有人在这里参与。 鉴于他们的反馈以及此处的讨论,遗憾的是,我认为我们应该暂停该提案。

与一般的协程或堆栈切换相比,Await 具有更简单的可观察行为,但我与之交谈的 VM 人员同意@rossberg的观点,即最终的 VM 工作可能对两者都相似。 并且至少一些 VM 人员相信无论如何我们都会获得协程或堆栈切换,并且我们可以使用它来支持 await 的用例。 这将意味着在每次调用 wasm 时创建一个新的协程/堆栈(与本提案不同),但至少一些 VM 人员认为这可以做得足够快。

除了 VM 人员缺乏兴趣之外,我们还对@fgmccabe@RossTate的这个提议提出了一些强烈反对,如上所述。 我们在某些事情上存在分歧,但我很欣赏这些观点,以及解释它们的时间。

总而言之,总的来说,尝试在这里前进是浪费每个人的时间。 但是感谢所有参与讨论的人! 希望至少这能激发协同程序/堆栈切换的优先级。

请注意,该提案的 JS 部分将来可能会相关,因为 JS 糖基本上是为了方便 Promise 集成。 我们需要等待堆栈切换或协程,看看这是否可以在此基础上工作。 但我认为不值得让这个问题保持开放,所以关闭。

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

frehberg picture frehberg  ·  6评论

mfateev picture mfateev  ·  5评论

badumt55 picture badumt55  ·  8评论

JimmyVV picture JimmyVV  ·  4评论

cretz picture cretz  ·  5评论