worker_threads 多线程的使用
当进行 CPU 密集型任务(如大数据量计算)的时候,node 的默认单线程机制无法做到性能的最佳释放,cpu 占用率上不去,其实 Node.js V10.5.0 版本开始就提供了 worker_threads
模块来支持多线程操作;我们用一个例子来介绍下多线程的使用方法,看看相对于单线程,性能的提升有多大。
首先设定一个场景,比如计算 12 个单词的全排列,这是一个阶乘问题。12 的阶乘 12! 约等于 4.79 亿种组合。阶乘计算可以用一个简单的函数来实现,如下:
阶乘计算
node
// 计算阶乘的方法
function factorial(n) {
if (n === 0) return 1;
return n * factorial(n - 1);
}
全排列函数计算
我们要定义一个生成器函数来生成所有的 4.79 亿种排列组合:
js
// 生成该阶乘对应的所有排列组合数据
// 可假设 wordsList = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l']
function* permute(wordsList) {
const permArr = [];
const usedChars = [];
function* permuteInternal(wordsList) {
let i, ch;
for (i = 0; i < wordsList.length; i++) {
ch = wordsList.splice(i, 1)[0];
usedChars.push(ch);
if (wordsList.length === 0) {
yield [...usedChars];
}
yield* permuteInternal(wordsList);
wordsList.splice(i, 0, ch);
usedChars.pop();
}
}
yield* permuteInternal(wordsList);
}
// 通过遍历器访问每一项
for (let permutation of permutations) {
console.log(permutation)
}
单线程效率日志
然后运行时通过打时间戳,可计算出运行速度,这一步省略,直接看结果日志:
以上是单线程执行的效果,可以看到每秒的处理速度为 41718 个左右,完成全部运算需要 3小时,下面我们引入多线程。
多线程主文件
主线程代码 (主文件),创建一个文件 main.js
,内容如下:
js
const os = require('os');
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
const workers = [];
let numCPUs = os.cpus().length; // 可根据当前设备的 cpu 核心数去决定使用多少线程
for (let i = 0; i < useNumCPUs; i++) {
const worker = new Worker(path.join(__dirname, 'workerEth.js'), {
workerData: {
knownWords: knownWordsArray,
targetAddress,
fileName,
progressNum,
start: i * permutationsPerWorker + progressNum,
end: (i + 1) * permutationsPerWorker + progressNum,
threadIndex: i + 1,
}
});
}
worker.on('message', (message) => {}) // 接收子进程发回的消息
worker.on('error', (error) => {
console.error(`Worker error: ${error}`);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker stopped with exit code ${code}`);
} else {
console.log('Worker stopped successfully.');
}
});
workers.push(worker); // 将所有线程都放到workers 里,以方便后面批量终止
}
多线程子线程文件
子线程文件代码概要:
js
const { parentPort, workerData, threadId } = require('worker_threads');
const { knownWords, fileName, progressNum, start, end, threadIndex } = workerData;
parentPort.postMessage({})
在这个示例中:
主线程代码使用 isMainThread
检查当前运行环境是否为主线程,如果是主线程,它将创建 Worker
实例,指向当前文件并传递一些数据 (workerData
)。
主线程还设置了一些事件处理器来处理来自 worker 的消息、错误和退出事件,Worker 子线程接收到传入的 workerData
,进行计算并通过 postMessage
将结果发送回主线程。
定义 workers 数组的目的是为了在需要统一终止所有线程时可通过循环批量处理:
js
function terminateAllWorkers() {
for (let worker of workers) {
worker.terminate();
}
}
多线程效率日志
从最终的运行日志可看到,当使用不同线程数量时,最终的运行效率也不同。

注意点:并不是有多少核心就用多少线程哦,要综合考虑,线程过多会导致发热降频,还会影响设备执行其他任务,甚至导致系统崩溃。
因为 Node 的多线程(通过 worker_threads
)与其他语言的多线程在设计理念和实现方式上有很大不同,Node.js 更注重简单性和易用性,通过非阻塞 I/O 和事件驱动模型来处理并发,从而避免了其他语言的线程同步、共享资源的竞争以及死锁等复杂问题。
http 并发问题
多线程虽然在 cpu 密集计算中能大大提升性能,不过也并不是万金油,在实际项目中性能往往受限于最短板(木桶效应 ),最常见的就是网络请求,因为 http 请求要涉及 TCP 握手、挥手、数据传输 等环节,耗时不稳定,可能在几毫秒到几秒钟之间,假设你购买了一项 api 服务,它支持 1000rps
(每秒请求数),如果你不针对并发进行优化,那么要么很快超过了 1000rps 限制服务被中断,要么同步执行导致业务阻塞,所以优化 http 请求也是可以提升整体程序性能的一个重要环节。
通常情况下,并发问题只是后端考虑的多,对单个前端终端来说,很少有大量批量请求的情况,不过少并不代表没有,当真的遇到这种情况时,怎么处理呢?
bottleneck 库用法
这里介绍一个 node 库 bottleneck
,它是一款适用于 Node.js 和浏览器的轻量级零依赖任务调度器和速率限制器, 在同类库中下载量最大。(不同npm包对比 点这里)

使用起来也很简单,示例代码如下:
js
// bottleneck 写法
const Bottleneck = require('bottleneck');
const MAX_CONCURRENT_REQUESTS = 500;
const dataList = Array.from({ length: 1000 }, (_, i) => i); // 示例数据列表
const limiter = new Bottleneck({
maxConcurrent: MAX_CONCURRENT_REQUESTS, // 最大并发数
minTime: 10 // 最小时间间隔
});
async function fetchInfo(i) {
// 模拟一个异步请求
return new Promise(resolve => setTimeout(resolve, Math.random() * 1000, i));
}
async function main() {
// 使用 Promise.all 来等待所有任务完成
await Promise.all(
dataList.map(async i => {
try {
await limiter.schedule(() => fetchInfo(i));
console.log(`任务 ${i} 完成`);
} catch (error) {
console.error(`任务 ${i} 失败:`, error);
}
})
);
console.log('所有任务完成');
}
main();
最重要的两个参数就是 maxConcurrent
和 minTime
,假设你购买的 api 服务支持 1000rps, 那就可以设置 maxConcurrent 为 1000, minTime 是最小请求间隔,理论上可以设置为 0 。
原生语法实现
上面 bottleneck
模块实现的功能,我们用原生语法也是可以实现的,代码如下:
js
// 原生写法
const MAX_CONCURRENT_REQUESTS = 500;
const tasks = [];
const ongoingTasks = new Set();
async function addTask(task) {
ongoingTasks.add(task);
task.finally(() => ongoingTasks.delete(task));
tasks.push(task);
}
async function fetchInfo(i) {
// 模拟一个异步请求
return new Promise(resolve => setTimeout(resolve, Math.random() * 1000, i));
}
async function main() {
for (let i of dataList) {
const task = fetchInfo(i);
await addTask(task);
if (ongoingTasks.size >= MAX_CONCURRENT_REQUESTS) {
await Promise.race(ongoingTasks); // 等待任意一个任务完成
}
}
await Promise.all(tasks);
}
const dataList = Array.from({ length: 1000 }, (_, i) => i);
main().then(() => console.log('所有任务完成'));
这两种写法上最大的区别是,bottleneck 库不需要手动控制最大并发数和时间间隔了,bottleneck 提供了 schedule
方法,用于调度任务,并自动处理并发控制和时间间隔,使用起来更简便。
而用原生写法,则需要手动控制并发请求数和时间间隔,这增加了代码复杂度,当达到最大并发数后要加 await 等待任意一个 api 任务完成,才能继续流程,任务队列补上下一个,时间间隔还需要额外增加逻辑处理。
总结
本文介绍了 node 性能优化的两种重要方法,希望对你们有帮助。