前言
WebAssembly 能够在浏览器内提供与原生机器码相当的性能,这对于一些需要大量计算的应用是非常有必要的。而当这一计算时间长到对用户有感知的时候,就需要使用多线程模型防止计算阻塞用户交互了。
JavaScript 本身仅支持单线程运行程序,而 Web Worker [1] 则提供了在后台运行指定程序的能力。
使用 Web Worker
Web Worker 需要将要执行的后台代码放在一个单独的文件中,并使用以下方法载入:
let myWorker = new Worker('worker.js');
Web Worker 是通过 postMessage() 方法和 onmessage 事件进行通讯的。例如:
main.js:
let worker = new Worker("worker.js"); // 创建 Worker
console.log("creating worker"); // 打个日志
worker.onmessage = (evt) => { // 注册消息事件
console.log("main onmessage", evt); // 收到后先打印对应消息
worker.postMessage("message from main"); // 然后向Worker再次发送回应
};
worker.js:
console.log("worker init."); // 打个日志
self.onmessage = (evt) => { // 注册消息事件
console.log("worker onmessage", evt); // 收到后打印对应消息
}
self.postMessage("message from worker"); // 执行完毕,向主线程发送消息
实际的控制台输出如下:
creating worker main.js:2:8
worker init. worker.js:1:9
main onmessage message { … } main.js:3:12
worker onmessage [object MessageEvent] worker.js:4:13
上面的代码展示了消息是如何在两个线程之间通信的,以及对应的事件顺序。
对于主线程而言,Web Worker 的初始化是非阻塞的,意味着其在 new 后可能还没有加载完毕,不能立即使用 postMessage 向 worker 发送消息。
在 Web Worker 中调用 Web Assembly
在使用 Rust 编译为 Web Assembly 后 [2],会得到以下文件:
- project.js: 生成的胶水代码,用于与下面的wasm交互,将js数据类型转换到wasm的数据类型
- project_bg.wasm:生成的实际wasm文件
为了在 Web Worker 中使用生成的 wasm 模块,就必须导入上述胶水代码,如:
import init, {some_function} from "your-wasm-module";
init();
self.onmessage = (evt) => {
some_function(evt.data);
};
但是很可惜的是,Firefox 目前暂时不支持在 Web Worker 中引入外部模块 [3]。在引入外部代码的时候,会直接报错。
wasm-bindgen 的官方帮助页面上提供了另一种方式 [4],通过将 wasm 和对应的 js 文件编译为传统的非模块文件,直接在页面中全局引入,从而避免报错。
停一下,停一下。这种开历史倒车的行为可不是什么好主意。我们现在有 Webpack 和 Vite 等打包工具,可以将依赖的模块打包到一个文件中,因此就没有外部模块引入了。
假设你正在使用 Vite,那么直接:
main.ts:
import MyWorker from './worker?worker';
let worker = new MyWorker();
// ...
worker.ts:
import init, {some_function} from "your-wasm-module";
init();
// ...
这样就可以了!
注意上面的import Worker的写法,根据vite官方文档[5] 和Github Issue [6] 的说法,你需要使用这种模块的方式引入并新建Worker。
不过在使用 Vite dev 的时候,由于其直接使用了原生的模块系统,所以无法在 Firefox 上正常使用。但是它打包出来的成品在 Firefox 上是没问题的,你也可以通过设置 flags 使 Firefox 启用这项实验性功能。
Web Worker 中的坑
Web Worker 使用中需要有一些注意的地方:
通过复制的传递无法传递 ArrayBuffer
Web Worker 使用的 postMessage 方法会将传入对象拷贝,再将其传递给 onmessage 方法。这一方法在 TypedArray(如 Uint8Array)上无法工作,其会传递一个空的 Array 过去,导致代码出现异常。
对于这种 Array,需要使用可转让对象传递数据 [7]。使用可转让对象会将整个对象的所有权交给对面,使用后对象即不再可用。
以下代码演示了如何使用可转让对象传递 Uint8Array:
let arr = new Uint8Array();
self.postMessage(arr, [arr.buffer]);
PostMessage 是队列执行的
对于同一个 Web Worker,若调用了多次 postMessage,其会在内部按队列顺序处理这些调用。具体来说,每次 onmessage 收到消息后,只有当消息处理完毕后,才会开始处理下一个消息。
0 条评论