Node.js 多进程处理CPU密集任务

Node.js 单线程与多进程

大家都知道 Node.js 性能很高,是以异步事件驱动、非阻塞 I/O 而被广泛使用。但缺点也很明显,由于 Node.js 是单线程程序,如果长时间运算,会导致 CPU 不能及时释放,所以并不适合 CPU 密集型应用。

当然,也不是没有办法解决这个问题。虽然 Node.js 不支持多线程,但是可创建多子进程来执行任务。 Node.js 提供了 child_processcluster 两个模块可用于创建多子进程

下面我们就分别使用单线程和多进程来模拟查找大量斐波那契数进行 CPU 密集测试

以下代码是查找 500 次位置为 35 的斐波那契数(方便测试,定了一个时间不需要太长也不会太短的位置)

单线程处理

代码:single.js

1function fibonacci(n) { 2 if (n == 0 || n == 1) { 3 return n; 4 } else { 5 return fibonacci(n - 1) + fibonacci(n - 2); 6 } 7} 8 9let startTime = Date.now(); 10let totalCount = 500; 11let completedCount = 0; 12let n = 35; 13 14for (let i = 0; i < totalCount; i++) { 15 fibonacci(n); 16 completedCount++; 17 console.log(`process: ${completedCount}/${totalCount}`); 18} 19console.log("👏 👏 👏 👏 👏 👏 👏 👏 👏 👏"); 20console.info(`任务完成,用时: ${Date.now() - startTime}ms`); 21console.log("👏 👏 👏 👏 👏 👏 👏 👏 👏 👏");

执行node single.js 查看结果

在我的电脑上显示结果为44611ms(电脑配置不同也会有差异)。

1... 2process: 500/500 3👏 👏 👏 👏 👏 👏 👏 👏 👏 👏 4任务完成,用时: 44611ms 5👏 👏 👏 👏 👏 👏 👏 👏 👏 👏

查找 500 次需要 44 秒,太慢了。可想而知如果位置更大,数量更多...

那我们来尝试用多进程试试 ⬇️

多进程

采用 cluster 模块,Master-Worker 模式来测试 共 3 个 js,分别为主线程代码:master.js、子进程代码:worker.js、入口代码:cluster.js(入口可无需单独写一个 js、这里是为了看起来更清楚一些)

主线程代码:master.js

1const cluster = require("cluster"); 2const numCPUs = require("os").cpus().length; 3 4// 设置子进程执行程序 5cluster.setupMaster({ 6 exec: "./worker.js", 7 slient: true 8}); 9 10function run() { 11 // 记录开始时间 12 const startTime = Date.now(); 13 // 总数 14 const totalCount = 500; 15 // 当前已处理任务数 16 let completedCount = 0; 17 // 任务生成器 18 const fbGenerator = FbGenerator(totalCount); 19 20 if (cluster.isMaster) { 21 cluster.on("fork", function(worker) { 22 console.log(`[master] : fork worker ${worker.id}`); 23 }); 24 cluster.on("exit", function(worker, code, signal) { 25 console.log(`[master] : worker ${worker.id} died`); 26 }); 27 28 for (let i = 0; i < numCPUs; i++) { 29 const worker = cluster.fork(); 30 31 // 接收子进程数据 32 worker.on("message", function(msg) { 33 // 完成一个,记录并打印进度 34 completedCount++; 35 console.log(`process: ${completedCount}/${totalCount}`); 36 37 nextTask(this); 38 }); 39 40 nextTask(worker); 41 } 42 } else { 43 process.on("message", function(msg) { 44 console.log(msg); 45 }); 46 } 47 48 /** 49 * 继续下一个任务 50 * 51 * @param {ChildProcess} worker 子进程对象,将在此进程上执行本次任务 52 */ 53 function nextTask(worker) { 54 // 获取下一个参数 55 const data = fbGenerator.next(); 56 // 判断是否已经完成,如果完成则调用完成函数,结束程序 57 if (data.done) { 58 done(); 59 return; 60 } 61 // 否则继续任务 62 // 向子进程发送数据 63 worker.send(data.value); 64 } 65 66 /** 67 * 完成,当所有任务完成时调用该函数以结束程序 68 */ 69 function done() { 70 if (completedCount >= totalCount) { 71 cluster.disconnect(); 72 console.log("👏 👏 👏 👏 👏 👏 👏 👏 👏 👏"); 73 console.info(`任务完成,用时: ${Date.now() - startTime}ms`); 74 console.log("👏 👏 👏 👏 👏 👏 👏 👏 👏 👏"); 75 } 76 } 77} 78 79/** 80 * 生成器 81 */ 82function* FbGenerator(count) { 83 var n = 35; 84 for (var i = 0; i < count; i++) { 85 yield n; 86 } 87 return; 88} 89 90module.exports = { 91 run 92};

1.这里是根据当前电脑的逻辑 CPU 核数来创建子进程的,不同电脑数量也会不一样,我的 CPU 是 6 个物理核数,由于支持超线程处理,所以逻辑核数是 12,故会创建出 12 个子进程

2.主线程与子进程之间通信是通过send方法来发送数据,监听message事件来接收数据

3.不知道大家有没有注意到我这里使用了 ES6 的 Generator 生成器来模拟生成每次需要查找的斐波那契数位置(虽然是写死的 😂,为了和上面的单线程保证统一)。这么做是为了不让所有任务一次性扔出去,因为就算扔出去也会被阻塞,还不如放在程序端就给控制住,完成一个,放一个。

子进程代码:worker.js

1function fibonacci(n) { 2 if (n == 0 || n == 1) { 3 return n; 4 } else { 5 return fibonacci(n - 1) + fibonacci(n - 2); 6 } 7} 8 9// 接收主线程发送过来的任务,并开始查找斐波那契数 10process.on("message", n => { 11 var res = fibonacci(n); 12 // 查找结束后通知主线程,以便主线程再度进行任务分配 13 process.send(res); 14});

入口代码:cluster.js

1// 引入主线程js,并执行暴露出来的run方法 2const master = require("./master"); 3master.run();

执行node cluster.js 查看结果

在我的电脑上显示结果为10724ms(电脑配置不同也会有差异)。

1process: 500/500 2👏 👏 👏 👏 👏 👏 👏 👏 👏 👏 3任务完成,用时: 10724ms 4👏 👏 👏 👏 👏 👏 👏 👏 👏 👏

结果

进过上面两种方式的对比,结果很明显,多进程处理速度是单线程处理速度的 4 倍多。而且有条件的情况下,如果电脑 CPU 足够,进程数更多,那么速度也会更快。

如果有更好的方案或别的语言能处理你的需求那就更好,谁让 Node.js 天生就不适合 CPU 密集型应用呢。。

阅读原文

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )