文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

压榨计算机性能:浏览器和 Node.js 的异步计算

2024-12-14 01:10

关注

本文转载自微信公众号「神光的编程秘籍」,作者神说要有光。转载本文请联系神光的编程秘籍公众号。

都说 Node.js 可以实现高性能的服务器,那什么是高性能呢?

所有的软件代码最终都是通过 CPU 来跑的,能不能把 CPU 高效利用起来是区分性能高低的标志,也就是说不能让它空转。

那什么时候会空转呢?

所以,要想达到高性能,就要解决这两个问题。

操作系统提供了线程的抽象,对应代码不同的执行分支,都是可以同时上不同的 CPU 跑的,这是利用好多核 CPU 性能的方式。

而如果有的线程在进行 IO 了,也就是要阻塞的等待读写完成,这种是比较低效的方式,所以操作系统实现了 DMA 的机制,就是设备控制器,由硬件来负责从设备到内存的搬运,在搬完了告诉 CPU 一声。这样当有的线程在 IO 的时候就可以把线程暂停掉,等收到 DMA 运输数据完成的通知再继续跑。

多线程、DMA,这是利用好多核 CPU 优势、解决 CPU 阻塞等 IO 的问题的操作系统提供的解决方案。

而各种编程语言对这种机制做了封装,Node.js 也是,Node.js 之所以是高性能,就是因为异步 IO 的设计。

Node.js 的异步 IO 的实现在 libuv,基于操作系统提供的异步的系统调用,这种一般是硬件级别的异步,比如 DMA 搬运数据。但是其中有一些同步的系统调用,通过 libuv 封装以后也会变成异步的,这是因为 libuv 内有个线程池,来执行这些任务,把同步的 API 变成异步的。这个线程池的大小可以通过 UV_THREADPOOL_SIZE 的环境变量设置,默认是 4。

我们在代码里调用的异步 API,很多都是通过线程来实现的。

比如:

  1. const fsPromises = require('fs').promises; 
  2.  
  3. const data = await fsPromises.readFile('./filename'); 

但是,这种异步 API 只解决了 IO 的问题,那如何利用多核 CPU 的优势来做计算呢?

Node.js 在 10.5 实验性的引入(在 12 正式引入)了 worker_thread 模块,可以创建线程,最终用多个 CPU 跑,这是利用多核 CPU 的做计算的方式。

异步 API 可以利用多线程做 IO,而 worker_thread 可以创建线程做计算,用于不同的目的。

要聊清楚 worker_thread,还得从浏览器的 web worker 聊起。

浏览器的 web worker

浏览器也同样面临不能利用多核 CPU 做计算的问题,所以 html5 引入了 web worker,可以通过另一个线程做计算。

我们创建一个 Worker 对象,指定跑在另一个线程的 js 代码,然后通过 postMessage 传递消息给它,通过 onMessage 接收消息。这个过程也是异步的,我们进一步把它封装成了 promise。

然后在 webWorker.js 里面接收数据,做计算,之后通过 postMessage 传回结果。

  1.  
  2. "en"
  3.  
  4.  
  5.      
  6.  
  7.  
  8.  

这样,我们就利用了另一个 CPU 核来跑了这段计算,对写代码来说和普通的异步代码没啥区别。但这个异步实际上不是 IO 的异步,而是计算的异步。

Node.js 的 worker thread 和 web worker 类似,我甚至怀疑 worker thread 的名字就是受 web worker 影响的。

Node.js 的 worker thread

把上面那段异步计算的逻辑在 Node.js 里面实现话,是这样的:

  1. const runCalcWorker = require('./runCalcWorker'); 
  2.  
  3. (async function () { 
  4.     const res = await runCalcWorker(2, 3, 3, 3); 
  5.     console.log(res); 
  6. })(); 

以异步的方式调用,因为异步计算和异步 IO 在使用方式上没啥区别。

  1. // runCalcWorker.js 
  2. const  { Worker } = require('worker_threads'); 
  3.  
  4. module.exports = function(...nums) { 
  5.     return new Promise(function(resolve, reject) { 
  6.         const calcWorker = new Worker('./nodeWorker.js'); 
  7.         calcWorker.postMessage(nums); 
  8.  
  9.         calcWorker.on('message', resolve); 
  10.         calcWorker.on('error', reject); 
  11.     }); 

然后异步计算的实现是通过创建 Worker 对象,指定在另一个线程跑的 JS,然后通过 postMessage 传递消息,通过 message 接收消息。这个和 web worker 很类似。

  1. // nodeWorker.js 
  2. const { 
  3.     parentPort 
  4. } = require('worker_threads'); 
  5.  
  6. parentPort.on('message', (data) => { 
  7.     const res = data.reduce((total, cur) => { 
  8.         return total += cur; 
  9.     }, 0); 
  10.     parentPort.postMessage(res); 
  11. }); 

在具体执行计算的 nodeWorker.js 里面,监听 message 消息,然后进行计算,通过 parentPost.postMessage 传回数据。

对比下 web worker,你会发现特别的像。所以,我觉得 Node.js 的 worker thread 的 api 是参考 web worker 来设计的。

但是,其实 worker thread 也支持在创建的时候就通过 wokerData 传递数据:

  1. const  { Worker } = require('worker_threads'); 
  2.  
  3. module.exports = function(...nums) { 
  4.     return new Promise(function(resolve, reject) { 
  5.         const calcWorker = new Worker('./nodeWorker.js', { 
  6.             workerData: nums 
  7.         }); 
  8.         calcWorker.on('message', resolve); 
  9.         calcWorker.on('error', reject); 
  10.     }); 

然后 worker 线程里通过 workerData 来取:

  1. const { 
  2.     parentPort, 
  3.     workerData 
  4. } = require('worker_threads'); 
  5.  
  6. const data = workerData; 
  7. const res = data.reduce((total, cur) => { 
  8.     return total += cur; 
  9. }, 0); 
  10. parentPort.postMessage(res); 

因为有个传递消息的机制,所以要做序列化和反序列化,像函数这种无法被序列化的数据就无法传输了。这也是 worker thread 的特点。

Node.js 的 worker thread 和 浏览器 web woker 的对比

从使用上来看,都可以封装成普通的异步调用,和其他异步 API 用起来没啥区别。

都要经过数据的序列化反序列化,都支持 postMessage、onMessage 来收发消息。

除了 message,Node.js 的 worker thread 支持传递数据的方式更多,比如还有 workerData。

但从本质上来看,两者都是为了实现异步计算,充分利用多核 CPU 的性能,没啥区别。

总结

高性能的程序也就是要充分利用 CPU 资源,不要让它空转,也就是 IO 的时候不要让 CPU 等,多核 CPU 也要能同时利用起来做计算。操作系统提供了线程、DMA的机制来解决这种问题。Node.js 也做了相应的封装,也就是 libuv 实现的异步 IO 的 API,但是计算的异步是 Node 12 才正式引入的,也就是 worker thread,API 设计参考了浏览器的 web worker,传递消息通过 postMessage、onMessage,需要做数据的序列化,所以函数是没法传递的。 

从使用上来看异步计算、异步 IO 使用方式一样,但是异步 IO 只是让 cpu 不同阻塞的等待 IO 完成,异步计算是利用了多核 CPU 同时进行并行的计算。

 

来源:神光的编程秘籍内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯