分店老板的烦恼:佛跳墙太耽误事
分店老板每天忙得脚不点地,一边招待客人、一边下单、一边炒菜。
偏偏有位常客,每次来都点佛跳墙 。这东西光备料和炖就要好几个小时,而且得一直有人盯着火候、搅锅、加料(比如大量的循环完全的占用CPU那种)。
老板要是自己蹲厨房炖佛跳墙亲自不停地搅拌,那大厅里所有客人全得饿死。因为他是单线程------炖佛跳墙的时候没法同时接单。
分店老板想:要是有个帮我搅拌佛跳墙的学徒就好了,我让他去炖,好了叫我一声;我自己继续在前厅忙活,两不耽误。
这个学徒就是 Worker Threads。
Worker Threads 就是分店里的专用学徒
- 学徒有独立的脑子(线程):他可以独立干活,不干扰老板的思路。
- 学徒有自己的灶台(独立内存):他炖糊了锅,不会让前台老板的锅也坏掉。
- 学徒和老板有对讲机(消息通道) :通过
postMessage告诉对方"材料""进度""炖好了"。 - 学徒听老板指挥:老板说"去炖",他才开始;炖完报告"好了",老板端走。
最关键的区别 :学徒和老板共享同一个分店的营业执照(同一个进程),但他们占用不同的 CPU 核心,所以能并行干活。这点和 Cluster 开新分店不同------那是独立的店(独立进程)。
代码当场开演:老板和学徒
javascript
// main.js ------ 分店老板(主线程)
const { Worker } = require('worker_threads');
console.log('老板:今天有客人点佛跳墙,叫学徒过来');
// 招一个学徒,告诉他工作脚本是 cook.js
const worker = new Worker('./cook.js');
// 给学徒发材料(传数据)
worker.postMessage({ dish: '佛跳墙', materials: ['鲍鱼', '海参', '花胶'] });
// 监听学徒的报告
worker.on('message', (msg) => {
console.log(`老板收到:${msg}`); // "佛跳墙炖好了!"
// 老板端出去给客人
});
worker.on('error', (err) => {
console.log('老板:学徒把厨房炸了', err);
});
worker.on('exit', (code) => {
console.log(`学徒收工,退出码:${code}`);
});
// 老板继续在前厅忙,不会被炖汤耽搁
console.log('老板继续接单、炒菜、收银...');
javascript
// cook.js ------ 学徒的脚本(另一个线程执行)
const { parentPort } = require('worker_threads');
// 收到老板的指令
parentPort.on('message', (msg) => {
console.log(`学徒:收到老板的材料,开始炖 ${msg.dish}`);
// 模拟超级耗时的炖煮(这会阻塞学徒自己的线程,但不影响老板)
const start = Date.now();
while (Date.now() - start < 5000) {
// 学徒在专心搅锅,不能分心
}
// 炖好了,通知老板
parentPort.postMessage(`${msg.dish} 炖好了!`);
});
运行 main.js,你会看到:
老板:今天有客人点佛跳墙,叫学徒过来
老板继续接单、炒菜、收银...
学徒:收到老板的材料,开始炖 佛跳墙
(5秒间隔,老板可以干其他事)
老板收到:佛跳墙 炖好了!
学徒收工,退出码:0
看,老板的代码一直没被阻塞。如果没有学徒,老板就得自己进去炖 5 秒,这期间别的客人全都干瞪眼。
共享内存:老板和学徒共用小白板
有时老板想随时知道"炖了多少了",学徒也需要看看新的配料清单,用 postMessage 一来一回太慢。
于是他们在墙上挂了块小白板(SharedArrayBuffer),两人都能看、都能写。
javascript
// main.js 老板这边
const { Worker } = require('worker_threads');
// 一块 4 字节的小白板
const sab = new SharedArrayBuffer(4);
const view = new Int32Array(sab);
const worker = new Worker('./cook.js', { workerData: sab });
console.log('老板:小白板初始值', view[0]); // 0
// 老板偶尔去看看进度
setInterval(() => {
console.log(`老板看一眼进度:${view[0]}%`);
if (view[0] >= 100) {
console.log('老板:学徒完工了,我去端菜');
process.exit();
}
}, 500);
javascript
// cook.js 学徒这边
const { parentPort, workerData } = require('worker_threads');
const view = new Int32Array(workerData); // 拿到同一块白板
// 模拟炖煮,不断更新进度
let progress = 0;
function simmer() {
if (progress >= 100) {
parentPort.postMessage('完成');
return;
}
progress += 10;
view[0] = progress; // 写在白板上
setTimeout(simmer, 300); // 炖一会儿
}
simmer();
这就实现了极低延迟的数据共享 ,但得小心死锁:两个人都写同一个地方容易冲突。所以一般用原子性 Atomics(好比两个人约定"我写字时你别写")来协调。
Worker Threads vs Cluster 对照表(连锁店版)
| 对比项 | Cluster 分店 | Worker Threads 学徒 |
|---|---|---|
| 比喻 | 开另一家分店,完全独立 | 店里的学徒,共用店面 |
| 进程/线程 | 独立进程 | 独立线程 |
| 内存 | 完全独立 | 可共享(SharedArrayBuffer) |
| 通信 | IPC(对讲机),消息序列化 | postMessage + 共享内存 |
| 用途 | 分摊大量独立请求 | 处理 CPU 密集重活 |
| 崩溃影响 | 只挂一家店,其他照常 | 学徒搞砸可能影响整个进程(需隔离) |
| 创建成本 | 高(新进程) | 相对低(线程) |
小吃店实战建议
- 客人多(高并发 I/O):开连锁分店(Cluster)。
- 佛跳墙多(CPU 密集重活):每个分店招学徒(Worker Threads)。
- 简单小菜(普通异步):老板的事件循环足够。
分店老板终于不再担心被佛跳墙锁死,服务质量又上了一层。