当前位置: 首页 > 后端技术 > Node.js

Node.js中的进程和线程

时间:2023-04-03 12:47:20 Node.js

线程和进程是计算机操作系统的基本概念。它们是程序员中的高频词。如何理解它们?Node.js中的进程和线程呢?1.进程与线程1.1、专业文本定义进程(Process),进程是计算机中关于数据集上运行活动的程序,它是系统进行资源分配和调度的基本单位,是系统运行的基础操作系统结构。进程是线程的容器。线程(Thread),线程是操作系统能够进行计算调度的最小单位,包含在进程中,是进程中实际运行的单位。1.2.通俗理解上面的描述比较生硬,看完可能看不懂,不利于理解和记忆。那么我们举一个简单的例子:假设你是一个快递站点的快递员。一开始,该站点负责该地区的居民不多,您是唯一一个捡拾物品的人。把包裹送到张三家,再到李四家取件,一件件的事情都要处理。这就是所谓的单线程,所有的工作都必须按顺序进行。后来这一带的住户多了,工地派了好几个兄弟到这一带,还有一个组长。你可以服务更多的居民。这称为多线程。组长为主线程,各兄弟为线程。快递站点使用的手推车等工具由站点提供,每个人都可以使用,不是只供一个人使用。这称为多线程资源共享。目前工地手推车只有一台,大家需要用到。这称为冲突。解决办法有很多种,排队等待或者其他兄弟用完后等待通知,这就叫线程同步。总公司有很多站点,每个站点的运行模式几乎完全一样,这叫多进程。总公司称为主进程,各站点称为子进程。总公司与站点之间,各站点之间,手推车相互独立,不能混用。这称为进程之间不共享资源。这些站可以通过电话或其他方式连接,这称为管道。站点之间还有其他协调方式,以方便完成更大的计算任务,这称为进程间同步。也可以看看阮一峰的《进程与线程的简单解释》。2.Node.js中的进程和线程Node.js是单线程服务,事件驱动和非阻塞I/O模型的语言特性使得Node.js高效轻量。优点是避免了频繁的线程切换和资源冲突;擅长I/O密集型操作(底层模块libuv通过多线程调用操作系统提供的异步I/O能力进行多任务执行),但是对于服务端的Node.js,可能有每秒处理数百个请求。面对CPU密集型请求时,由于是单线程模式,难免会造成阻塞。2.1、Node.js阻塞我们使用Koa简单搭建一个web服务,使用斐波那契数列的方法来模拟Node.js如何处理CPU密集型计算任务:Fibonacci数列,又称黄金分割数列,本系列开始从第三项开始,每一项等于前两项:0,1,1,2,3,5,8,13,21,......//app.jsconstkoa=require('koa')constrouter=require('koa-router')()constapp=newKoa()//用于测试是否被阻塞router.get('/test',(ctx)=>{ctx.body={pid:process.pid,msg:'HelloWorld'}})router.get('/fibo',(ctx)=>{const{num=38}=ctx.queryconststart=Date.now()//斐波那契数列constfibo=(n)=>{returnn>1?fibo(n-1)+fibo(n-2):1}fibo(num)ctx.body={pid:process.pid,duration:Date.now()-start}})app.use(router.routes())app.listen(9000,()=>{console.log('Serverisrunningon9000')})执行节点应用程序。js启动服务,使用Postman发送请求。可以看到38次计算耗时617ms。也就是说,因为执行了一个CPU密集型的计算任务,Node.js主线程被阻塞了六百毫秒。如果同时处理的请求较多,或者计算任务比较复杂,这些请求之后的所有请求都会延迟执行。让我们创建一个新的axios.js来模拟发送多个请求。此时,将app.js中的fibo计算次数改为43次,模拟更复杂的计算任务://axios.jsconstaxios=require('axios')conststart=Date.now()constfn=(url)=>{axios.get(`http://127.0.0.1:9000/${url}`).then((res)=>{console.log(res.data,`耗时:${Date.now()-start}ms`)})}fn('test')fn('fibo?num=43')fn('test')可以看出,当请求需要进行CPU密集型计算时任务,后续请求被阻塞并等待。如果这样的请求太多了,服务基本上就会被阻塞卡住。对于这个不足,Node.js一直在弥补。2.2.master-workermaster-worker模式是一种并行模式。核心思想是:当系统有两个或多个进程或线程协同工作时,master负责接收、分配和整合任务,worker负责处理任务。2.3.多线程线程是CPU调度的基本单元。它只能同时执行一个线程的任务,同一个线程只能被一个CPU调用。如果您使用的是多核CPU,您将无法充分发挥CPU的性能。多线程给我们带来了灵活的编程方式,但是我们需要学习更多的Api知识,写的代码越多,风险也越大。线程切换和锁也会增加系统资源的开销。worker_threads工作线程,为Node.js提供真正的多线程能力。worker_threads是Node.js提供的多线程API。对于执行CPU密集型计算任务很有用,对于I/O密集型操作帮助不大,因为Node.js内置的异步I/O操作比worker_threads更高效。worker_threads中的worker,parentPort主要用于子线程和主线程的消息交互。稍微修改app.js将CPU密集型计算任务分配给子线程://app.jsconstKoa=require('koa')constrouter=require('koa-router')()const{Worker}=require('worker_threads')constapp=newKoa()//用于测试是否阻塞router.get('/test',(ctx)=>{ctx.body={pid:process.pid,msg:'HelloWorld'}})router.get('/fibo',async(ctx)=>{const{num=38}=ctx.queryctx.body=awaitasyncFibo(num)})constasyncFibo=(num)=>{returnnewPromise((resolve,reject)=>{//创建工作线程并传递数据constworker=newWorker('./fibo.js',{workerData:{num}})//主线程监听子线程发送的消息worker.on('message',resolve)worker.on('error',reject)worker.on('exit',(code)=>{if(code!==0)reject(newError(`Worker以退出代码${code}`))})})}app.use(router.routes())app.listen(9000,()=>{console.log('服务器正在运行在9000')})添加fibo.js文件来处理复杂的计算任务:const{workerData,parentPort}=require('worker_threads')const{num}=workerDataconststart=Date.now()//斐波那契数列constfibo=(n)=>{returnn>1?fibo(n-1)+fibo(n-2):1}fibo(num)parentPort.postMessage({pid:process.pid,duration:Date.now()-start})执行上面的axios.js,在这次app.js中的fibo计算次数改为43,模拟更复杂的计算任务:可以看到当CPU密集型计算任务交给子线程处理时,主线程没有不再阻塞,只需要等待子线程处理完成后,主线程只需要接收子线程返回的结果,其他请求不再受影响,有资源开销。线程是CPU调度的基本单位。它只能同时执行一个线程的任务,同一个线程只能被一个CPU调用。让我们重新回顾一下本节开头提到的线程和CPU描述。这时因为是新线程,可以在其他CPU核上执行,可以充分利用多核CPU。2.4.多进程Node.js提供了cluster模块,以充分利用CPU的多核能力。集群可以通过一个父进程管理多个子进程来实现集群功能。child_process子进程,生成一个新的Node.js进程,并使用已建立的IPC通信通道调用指定的模块。cluster集群可以创建共享server端口的子进程,worker进程使用child_process的fork方法派生。集群底层是child_process,master进程是master进程,启动一个agent进程和n个worker进程,agent进程处理一些公共事务,比如日志等;worker进程使用已建立的IPC(Inter-ProcessCommunication)通信通道与master进程通信,与master进程共享服务端口。添加fibo-10.js以模拟发送10个请求://fibo-10.jsconstaxios=require('axios')consturl=`http://127.0.0.1:9000/fibo?num=38`conststart=Date.now()for(leti=0;i<10;i++){axios.get(url).then((res)=>{console.log(res.data,`耗时:${Date.//app.jsconstcluster=require('cluster')consthttp=require('http')constnumCPUs=require('os').cpus().length//constnumCPUs=10//工作进程的数量一般与CPU核数相同constkoa=require('koa')constrouter=require('koa-router')()constapp=newKoa()//用于测试是否被阻塞router.get('/test',(ctx)=>{ctx.body={pid:process.pid,msg:'HelloWorld'}})router.get('/fibo',(ctx)=>{const{num=38}=ctx.queryconststart=Date.now()//斐波那契数列constfibo=(n)=>{returnn>1?fibo(n-1)+fibo(n-2):1}fibo(num)ctx.body={pid:进程.pid,duration:Date.now()-start}})app.use(router.routes())if(cluster.isMaster){console.log(`Master${process.pid}isrunning`)//spawnworkerprocessfor(leti=0;i{console.log(`worker${worker.process.pid}died`)})}else{app.listen(9000)console.log(`Worker${process.pid}started`)}执行nodeapp.js启动服务,可以看到,cluster帮我们创建了1个master进程和4个worker进程:通过fibo-10.js模拟发送10个请求,可以看到4个进程处理10个请求耗时将近9秒:启动10个worker进程时,看下效果:只需要不到3秒,但进程数不是无限的。在日常开发中,worker进程数一般与CPU核数相同。2.5.多进程说明开启多进程并不都是为了应对高并发,而是为了解决Node.js对多核CPU利用率不足的问题。通过fork方法从父进程派生出来的子进程拥有与父进程相同的资源,但又相互独立,不共享资源。通常进程数是根据CPU核数来设置的,因为系统资源是有限的。三、总结1、大部分通过多线程解决CPU密集型计算任务的方案都可以用多进程方案代替;2、Node.js虽然是异步的,但不代表不会阻塞。CPU密集型任务最好不要运行在主线程处理,以保证主线程的畅通;3、不要一味追求高性能和高并发,满足系统需求即可。高效和敏捷是项目需要的,这也是Node.js的轻量级特性。4.Node.js中还有很多进程和线程的概念在文中提到但没有详细阐述或没有提及,例如:Node.js底层I/O的libuv,IPC通信通道,如何守卫多进程,如何处理定时任务、代理进程等,进程间不共享资源;5、以上代码可以在https://github.com/liuxy0551/node-process-thread查看。