前言

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 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用 * 标注