JavaScript的多线程技术与传统编程语言多线程技术的区别
- 由于语言机制的限制,JavaScript中的线程之间难以共享内存(可以理解为JavaScript中的变量基本存储于线程栈中),这减少线程间的并发同步的问题,保证了JS线程的安全性。
- Node.js不支持fork进程,与Unix系统调用fork()不同,child_process模块的fork()函数不会克隆当前的进程,只是单纯地创建一个node实例。
- JS线程之间的数据共享基于对象深拷贝技术,无法共享全部对象,比如函数,因此,它们之间通过事件机制传递消息。
Node:使用Worker Threads模块
- 启动线程,作为一个独立的JavaScript执行线程,必须指定一个入口文件,防止读写其它线程的数据。
const { Worker } = require('worker_threads');
const th = new Worker(__dirname + '/task.js');
th.on('message', data => {
// handle data
});
启动工作线程时可以传递克隆的对象:
const worker = require('worker_threads');
if (worker.isMainThread) {
const th = new worker.Worker(__filename, {
workerData: [{ msg: 'hello', }, { info: 'world'}],
});
} else {
console.log(worker.workerData);
}
/**
工作线程接收到了父线程传递的克隆数组
[ { msg: 'hello' }, { info: 'world' } ]
*/
- 事件
父子线程之间使用事件传递消息,事件类型如下:
事件名称 | 描述 |
---|---|
message | 当子线程调用parentPort.postMessage(data: any) 时产生该事件,跨线程接收克隆的data。 |
exit | 当子线程调用parentPort.close() 时产生该事件,该事件只会产生一次,后续调用将被忽略。 |
online | 当子线程开始执行时产生该事件。 |
error | 当子线程抛出异常时产生该事件。 |
不过父进程只能向子线程发送message
事件,以及调用terminate()
终止子线程。
同时,父子之间可以使用emit(event: string, ...args)
模拟对方给自己发送消息,从而主动调用事件处理逻辑。
- Usage
const child_process = require('child_process');
const worker = require('worker_threads');
const express = require('express');
const colors = require('colors');
const { log, table, error } = console;
if (worker.isMainThread) {
try {
main();
} catch(e) {
info(e.message);
}
} else {
try {
task();
} catch(e) {
info(e.message);
}
}
return;
// Functions
function info() {
const list = [];
[...arguments].forEach(it => {
list.push(it.toString().rainbow);
});
log(...list);
}
function main() {
const app = new express();
app.listen(8080);
app.use((req, res, next) => {
const th = new worker.Worker(__filename, {
workerData: {
msg: '您的抽奖号码为:',
},
});
info('产生工作线程', th.threadId);
th.once('message', data => {
info('工作线程', th.threadId, '计算完毕,', '主线程开始回应客户端');
res.send(data);
});
});
}
function task() {
console.time(worker.threadId);
info('工作线程', worker.threadId, '开始执行IO或CPU密集任务');
child_process.execSync('sleep 2');
worker.parentPort.postMessage([
{ index: worker.threadId, result: worker.workerData.msg + Math.round(Math.random()*100), },
]);
console.timeEnd(worker.threadId);
}
浏览器
# index.js
(function main() {
const th = new Worker('./a.js');
th.onmessage = event => {
console.log(event.data);
};
console.table(th);
})();
# a.js
postMessage({
msg: 'good',
});
主线程看Worker
worker: Worker
onerror: null
onmessage: null
__proto__: Worker
onerror: (...)
onmessage: (...)
postMessage: ƒ postMessage()
terminate: ƒ terminate()
constructor: ƒ Worker()
Symbol(Symbol.toStringTag): "Worker"
get onerror: ƒ onerror()
set onerror: ƒ onerror()
get onmessage: ƒ onmessage()
set onmessage: ƒ onmessage()
__proto__: EventTarget
__proto__: Object
好明显, 只有四个函数: set onmessage()
, set onerror()
, postMessage()
, terminate()
.
显然任务的执行应该是不尽相同的,具体由主线程提供参数来决定,postMessage()
就起这个作用,当一个Worker脚本执行时,它应该在完成必要的初始化操作后立即进入监听状态,等待主线程的消息,从而触发不同的任务.
工作线程看Worker
工作线程中有三个方式访问worker引用: self
, this
, 或者像window一样直接访问其worker字段. 不过,建议使用globalThis
,这在Node.js中包括woker_thread中通用.
它主要通过set onmessage()
和postMessage()
与主线程通信.
Worker 线程能够访问一个全局函数importScripts()来引入脚本,该函数接受0个或者多个URI作为参数来引入资源;以下例子都是合法的:
importScripts(); /* 什么都不引入 */
importScripts('foo.js'); /* 只引入 "foo.js" */
importScripts('foo.js', 'bar.js'); /* 引入两个脚本 */
浏览器加载并运行每一个列出的脚本。每个脚本中的全局对象都能够被 worker 使用。如果脚本无法加载,将抛出 NETWORK_ERROR 异常,接下来的代码也无法执行。而之前执行的代码(包括使用 window.setTimeout() 异步执行的代码)依然能够运行。importScripts() 之后的函数声明依然会被保留,因为它们始终会在其他代码之前运行。
脚本的下载顺序不固定,但执行时会按照传入 importScripts() 中的文件名顺序进行。这个过程是同步完成的;直到所有脚本都下载并运行完毕,importScripts() 才会返回。
共享worker
参考
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers