在使用 Web Worker 和 SharedArrayBuffer 时,很多开发者会遇到类似现象:
worker.postMessage({
buffer: sharedBuffer
});
console.log('主线程后续代码');
即使 postMessage() 写在前面,控制台中也可能先看到:
主线程后续代码
Worker 收到消息
这并不是 SharedArrayBuffer 失效,也不是浏览器执行顺序混乱,而是由浏览器的事件循环、任务队列和线程调度机制共同决定的。
本文将系统介绍:
postMessage()到底做了什么为什么消息处理函数不会立即执行
SharedArrayBuffer为什么不会自动阻塞如何正确使用
Atomics实现同步postMessage和SharedArrayBuffer应该如何配合使用
一、先看一个简单示例
主线程
const worker = new Worker('./worker.js');
const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);
sharedArray[0] = 100;
worker.postMessage({
buffer: sharedBuffer
});
console.log('主线程:postMessage 之后执行');
Worker 线程
self.onmessage = function (event) {
const { buffer } = event.data;
const array = new Int32Array(buffer);
console.log('Worker:收到消息');
console.log('共享数据:', array[0]);
};
通常输出顺序是:
主线程:postMessage 之后执行
Worker:收到消息
很多人会认为:
worker.postMessage(...)
应该立即触发 Worker 中的:
self.onmessage = ...
但实际上并不是这样。
二、postMessage() 不是函数调用,而是发送异步消息
调用:
worker.postMessage(data);
并不会直接调用 Worker 中的 onmessage 函数。
它的主要作用是:
将数据发送给 Worker;
浏览器对消息进行结构化处理;
将消息放入 Worker 的事件队列;
等待 Worker 线程调度并处理这条消息。
可以简单理解为:
主线程:
postMessage()
↓
消息进入 Worker 队列
↓
Worker 未来某个时间处理消息
因此,postMessage() 本身是一个异步通信接口。
它只负责“提交消息”,而不是“等待消息处理完成”。
三、为什么 postMessage() 后面的代码会先执行?
看下面的代码:
worker.postMessage('hello');
console.log('A');
实际过程大致如下:
1. 主线程调用 worker.postMessage()
2. 浏览器把消息放入 Worker 的任务队列
3. postMessage() 立即返回
4. 主线程继续执行 console.log('A')
5. 主线程当前任务结束
6. Worker 被调度执行消息处理函数
所以输出通常是:
A
Worker 收到消息
关键原因是:
postMessage()只负责发送消息,不负责等待接收方执行完毕。
这和下面的同步函数调用完全不同:
function run() {
console.log('B');
}
run();
console.log('A');
同步函数调用时,run() 会立即执行完成后,才执行下一行。
而 postMessage() 更接近:
queue.push(message);
return;
接收方什么时候处理消息,由浏览器的事件循环和线程调度决定。
四、SharedArrayBuffer 并不会改变异步执行模型
SharedArrayBuffer 的作用是:
允许主线程和 Worker 共同访问同一块内存。
例如:
const sharedBuffer = new SharedArrayBuffer(1024);
const array = new Int32Array(sharedBuffer);
将它传递给 Worker:
worker.postMessage(sharedBuffer);
Worker 可以创建一个指向同一块内存的视图:
self.onmessage = event => {
const array = new Int32Array(event.data);
console.log(array[0]);
};
这里的“共享”指的是:
主线程和 Worker 看到的是同一块底层内存;
一方修改数据,另一方可以看到修改结果;
不需要复制整个缓冲区。
但是,SharedArrayBuffer 并不意味着:
主线程会等待 Worker;
Worker 会立即执行;
postMessage()会变成同步调用;对共享数据的读写天然安全;
不需要使用
Atomics。
也就是说:
SharedArrayBuffer解决的是“共享内存”问题,而不是“执行顺序”问题。
五、共享内存和线程同步是两件不同的事情
假设主线程执行:
const sharedBuffer = new SharedArrayBuffer(4);
const array = new Int32Array(sharedBuffer);
array[0] = 0;
worker.postMessage(sharedBuffer);
array[0] = 1;
Worker 中:
self.onmessage = event => {
const array = new Int32Array(event.data);
console.log(array[0]);
};
Worker 可能输出:
1
因为 Worker 真正执行 onmessage 时,主线程很可能已经执行完:
array[0] = 1;
这并不是 Worker 读取错误,而是因为:
postMessage()只是发送通知;主线程继续向下执行;
Worker 稍后才处理消息;
Worker 读取的是共享内存当前的值。
如果你希望 Worker 读取的是某个确定时刻的数据,就不能只依赖 postMessage() 的调用位置,而应该设计明确的同步协议。
六、postMessage 能保证什么顺序?
对于同一个消息发送方和同一个接收方,消息通常会按照发送顺序进行处理:
worker.postMessage('message 1');
worker.postMessage('message 2');
worker.postMessage('message 3');
Worker 一般会按照以下顺序收到:
message 1
message 2
message 3
但是,这种顺序保证只表示:
消息进入接收方后的处理顺序是有序的。
它不表示:
主线程会等待 Worker 执行;
Worker 会在主线程下一行代码之前执行;
共享内存中的读写会自动形成同步关系;
多个线程之间的普通读写不会发生竞争。
例如:
worker.postMessage('start');
sharedArray[0] = 100;
console.log('主线程完成');
不能简单理解为:
Worker 一定先看到 postMessage 时的值
因为消息到达和共享内存后续读写之间,需要通过明确的同步机制建立关系。
七、普通读写可能存在数据竞争
下面是一个典型问题:
主线程
sharedArray[0] = 0;
worker.postMessage(sharedBuffer);
sharedArray[0] = 1;
Worker
self.onmessage = event => {
const array = new Int32Array(event.data);
if (array[0] === 0) {
console.log('读取到 0');
} else {
console.log('读取到 1');
}
};
Worker 读取到什么值,取决于两个线程的实际调度时间。
这就是并发程序中的竞态条件。
如果多个线程同时修改共享数据,还可能出现:
sharedArray[0]++;
这实际上不是一个不可分割的操作,而是:
读取
加一
写回
如果两个线程同时执行,就可能发生数据丢失。
例如:
线程 A 读取 0
线程 B 读取 0
线程 A 写入 1
线程 B 写入 1
最终结果是 1,而不是预期的 2。
八、使用 Atomics 保证原子操作
对于共享的整数类型数据,可以使用 Atomics:
Atomics.add(sharedArray, 0, 1);
它可以保证加法操作是原子的,不会被其他线程拆开。
常用方法包括:
Atomics.load(array, index);
Atomics.store(array, index, value);
Atomics.add(array, index, value);
Atomics.sub(array, index, value);
Atomics.exchange(array, index, value);
Atomics.compareExchange(array, index, expected, replacement);
例如:
Atomics.store(sharedArray, 0, 100);
const value = Atomics.load(sharedArray, 0);
console.log(value);
相比普通读写:
sharedArray[0] = 100;
const value = sharedArray[0];
Atomics 能够提供更明确的并发访问语义。
九、使用 Atomics.wait() 等待线程通知
如果需要让 Worker 等待某个条件,可以使用:
Atomics.wait()
例如,主线程先准备数据,然后通知 Worker。
主线程
const buffer = new SharedArrayBuffer(8);
const state = new Int32Array(buffer);
const worker = new Worker('./worker.js');
worker.postMessage(buffer);
setTimeout(() => {
Atomics.store(state, 0, 123);
// 通知等待中的 Worker
Atomics.notify(state, 0, 1);
}, 1000);
Worker
self.onmessage = event => {
const state = new Int32Array(event.data);
console.log('Worker 开始等待');
Atomics.wait(state, 0, 0);
const value = Atomics.load(state, 0);
console.log('Worker 被唤醒,读取到:', value);
};
执行过程是:
1. Worker 收到消息
2. Worker 调用 Atomics.wait()
3. Worker 进入等待状态
4. 主线程写入共享数据
5. 主线程调用 Atomics.notify()
6. Worker 被唤醒
7. Worker 读取最新数据
这里才建立了明确的线程同步关系。
注意:主线程不能使用阻塞式 Atomics.wait()
浏览器主线程调用:
Atomics.wait(...)
通常会抛出异常,因为这会阻塞页面,导致:
页面无法渲染;
用户操作无响应;
浏览器标签页卡死。
因此:
Worker 中可以使用
Atomics.wait();主线程不要使用阻塞式等待;
主线程可以考虑使用
Atomics.waitAsync(),或者通过postMessage、Promise 等异步方式通知。
十、推荐模式:postMessage 负责传递,SharedArrayBuffer 负责共享,Atomics 负责同步
一个更合理的架构是:
postMessage:
传递 SharedArrayBuffer、任务参数、初始化信息
SharedArrayBuffer:
存储共享状态和数据
Atomics:
保证状态修改的原子性
实现等待和唤醒
例如设计一个状态值:
const STATUS_IDLE = 0;
const STATUS_READY = 1;
const STATUS_DONE = 2;
主线程
const buffer = new SharedArrayBuffer(16);
const shared = new Int32Array(buffer);
const worker = new Worker('./worker.js');
worker.postMessage(buffer);
// 写入任务数据
Atomics.store(shared, 1, 42);
// 设置为 READY
Atomics.store(shared, 0, STATUS_READY);
// 通知 Worker
Atomics.notify(shared, 0, 1);
Worker
self.onmessage = event => {
const shared = new Int32Array(event.data);
// 等待状态变为 READY
while (Atomics.load(shared, 0) !== STATUS_READY) {
Atomics.wait(shared, 0, STATUS_IDLE);
}
const input = Atomics.load(shared, 1);
console.log('Worker 收到任务:', input);
const result = input * 2;
Atomics.store(shared, 1, result);
// 设置为 DONE
Atomics.store(shared, 0, STATUS_DONE);
// 通知主线程
self.postMessage('done');
};
这里要注意:
postMessage(buffer)只负责把共享缓冲区交给 Worker;Atomics.store()写入状态;Atomics.wait()让 Worker 等待;Atomics.notify()唤醒 Worker;postMessage('done')可以作为完成通知。
十一、如何等待 Worker 执行完成?
如果你只是想知道 Worker 什么时候处理完成,通常不需要直接依赖 SharedArrayBuffer,使用普通消息即可:
主线程
worker.postMessage({
type: 'start',
buffer: sharedBuffer
});
worker.onmessage = event => {
if (event.data.type === 'done') {
console.log('Worker 执行完成');
}
};
console.log('主线程继续执行');
Worker
self.onmessage = event => {
const { type, buffer } = event.data;
if (type === 'start') {
const array = new Int32Array(buffer);
// 执行任务
array[0] = array[0] * 2;
self.postMessage({
type: 'done'
});
}
};
这种模式的优点是:
简单;
不阻塞主线程;
易于理解;
适合大多数任务完成通知场景。
只有当你需要高性能、低延迟、频繁共享数据或实现生产者消费者模型时,才需要深入使用 Atomics。
十二、SharedArrayBuffer 还需要满足安全条件
现代浏览器通常要求页面处于跨源隔离环境,才能使用 SharedArrayBuffer。
服务器需要设置:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
例如:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
前端可以检查:
console.log(crossOriginIsolated);
如果输出:
true
说明当前页面处于跨源隔离状态。
否则可能出现:
ReferenceError: SharedArrayBuffer is not defined
或者浏览器拒绝创建共享缓冲区。
十三、常见误区
误区一:postMessage 调用后,Worker 会立即执行
错误理解:
worker.postMessage(data);
// Worker 此时已经执行完成
正确理解:
worker.postMessage(data);
// 消息已经提交,但 Worker 可能还没有开始执行
误区二:SharedArrayBuffer 会自动同步
错误理解:
主线程写入数据后,Worker 一定按照期望顺序读取
正确理解:
SharedArrayBuffer 只提供共享内存;
同步需要使用 Atomics 或消息协议。
误区三:看到共享数据就不需要传消息
虽然 Worker 可以访问共享数据,但它仍然需要知道:
什么时候有新数据;
哪一段数据可读;
哪一段空间可写;
任务是否完成。
因此通常仍然需要:
postMessageAtomics.waitAtomics.notify状态标记
环形队列等机制
误区四:普通 array[index]++ 是安全的
在多线程环境中:
array[0]++;
不是原子操作。
应改为:
Atomics.add(array, 0, 1);
十四、总结
在浏览器中使用 SharedArrayBuffer 时,postMessage() 后面的代码先执行,是正常现象,主要原因如下:
postMessage()是异步消息通信,不会立即执行接收方代码;消息会先进入 Worker 的事件队列;
主线程会继续执行当前任务中的后续代码;
Worker 何时执行取决于浏览器调度和线程运行状态;
SharedArrayBuffer只负责共享内存,不负责线程同步;需要确定执行顺序时,应使用
Atomics.load()、Atomics.store()、Atomics.wait()和Atomics.notify();如果只是等待 Worker 完成,直接使用 Worker 的
postMessage回传结果通常更简单。
可以用一句话概括:
postMessage负责“通知”,SharedArrayBuffer负责“共享数据”,Atomics负责“同步执行顺序”。
理解这三者的职责之后,就不会再把“共享内存”和“同步执行”混为一谈了。