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