zoukankan      html  css  js  c++  java
  • nodejs中的子进程,深入解析child_process模块和cluster模块

      node遵循的是单线程单进程的模式,node的单线程是指js的引擎只有一个实例,且在nodejs的主线程中执行,同时node以事件驱动的方式处理IO等异步操作。node的单线程模式,只维持一个主线程,大大减少了线程间切换的开销。

      但是node的单线程使得在主线程不能进行CPU密集型操作,否则会阻塞主线程。对于CPU密集型操作,在node中通过child_process可以创建独立的子进程,父子进程通过IPC通信,子进程可以是外部应用也可以是node子程序,子进程执行后可以将结果返回给父进程。

      此外,node的单线程,以单一进程运行,因此无法利用多核CPU以及其他资源,为了调度多核CPU等资源,node还提供了cluster模块,利用多核CPU的资源,使得可以通过一串node子进程去处理负载任务,同时保证一定的负载均衡型。本文从node的单线程单进程的理解触发,介绍了child_process模块和cluster模块,本文的结构安排如下:


    • node中的单线程和单进程
    • node中的child_process模块实现多进程
    • node中的cluster模块
    • 总结

    一、node中的单线程和单进程

      首先要理解的概念是,node的单线程和单进程的模式。node的单线程于其他语言的多线程模式相比,减小了线程间切换的开销,以及在写node代码的时候不用考虑锁以及线程池的问题。node宣称的单线程模式,比其他语言更加适合IO密集型操作。那么一个经典的问题是:

    node是真的单线程的吗?

    提到node,我们就可以立刻想到单线程、异步IO、事件驱动等字眼。首先要明确的是node真的是单线程的吗,如果是单线程的,那么异步IO,以及定时事件(setTimeout、setInterval等)又是在哪里被执行的。

    严格来说,node并不是单线程的。node中存在着多种线程,包括:

    • js引擎执行的线程
    • 定时器线程(setTimeout, setInterval)
    • 异步http线程(ajax)

    ....

      我们平时所说的单线程是指node中只有一个js引擎在主线程上运行。其他异步IO和事件驱动相关的线程通过libuv来实现内部的线程池和线程调度。libv中存在了一个Event Loop,通过Event Loop来切换实现类似于多线程的效果。简单的来讲Event Loop就是维持一个执行栈和一个事件队列,当前执行栈中的如果发现异步IO以及定时器等函数,就会把这些异步回调函数放入到事件队列中。当前执行栈执行完成后,从事件队列中,按照一定的顺序执行事件队列中的异步回调函数。

    default

    上图中从执行栈,到事件队列,最后事件队列中按照一定的顺序执行回调函数,整个过程就是一个简化版的Event Loop。此外回调函数执行时,同样会生成一个执行栈,在回调函数里面还有可能嵌套异步的函数,也就是说执行栈存在着嵌套。

    也就是说node中的单线程是指js引擎只在唯一的主线程上运行,其他的异步操作,也是有独立的线程去执行,通过libv的Event Loop实现了类似于多线程的上下文切换以及线程池调度。线程是最小的进程,因此node也是单进程的。这样就解释了为什么node是单线程和单进程的。

    二、node中的child_process模块实现多进程

      node是单进程的,必然存在一个问题,就是无法充分利用cpu等资源。node提供了child_process模块来实现子进程,从而实现一个广义上的多进程的模式。通过child_process模块,可以实现1个主进程,多个子进程的模式,主进程称为master进程,子进程又称工作进程。在子进程中不仅可以调用其他node程序,也可以执行非node程序以及shell命令等等,执行完子进程后,以流或者回调的形式返回。

    1、child_process模块提供的API

    child_process提供了4个方法,用于新建子进程,这4个方法分别为spawn、execFile、exec和fork。所有的方法都是异步的,可以用一张图来描述这4个方法的区别。

    default

    上图可以展示出这4个方法的区别,我们也可以简要介绍这4中方法的不同。

    • spawn : 子进程中执行的是非node程序,提供一组参数后,执行的结果以流的形式返回。
    • execFile:子进程中执行的是非node程序,提供一组参数后,执行的结果以回调的形式返回。
    • exec:子进程执行的是非node程序,传入一串shell命令,执行后结果以回调的形式返回,与execFile
      不同的是exec可以直接执行一串shell命令。
    • fork:子进程执行的是node程序,提供一组参数后,执行的结果以流的形式返回,与spawn不同,fork生成的子进程只能执行node应用。接下来的小节将具体的介绍这一些方法。

    2、execFile和exec

    我们首先比较execFile和exec的区别,这两个方法的相同点:

    执行的是非node应用,且执行后的结果以回调函数的形式返回。

    不同点是:

    exec是直接执行的一段shell命令,而execFile是执行的一个应用

    举例来说,echo是UNIX系统的一个自带命令,我们直接可以在命令行执行:

    echo hello world
    

    结果,在命令行中会打印出hello world.

    (1) 通过exec来实现

    新建一个main.js文件中,如果要使用exec方法,那么则在该文件中写入:

    let cp=require('child_process');
    cp.exec('echo hello world',function(err,stdout){
      console.log(stdout);
    });
    

    执行这个main.js,结果会输出hello world。我们发现exec的第一个参数,跟shell命令完全相似。

    (2)通过execFile来实现

    let cp=require('child_process');
    cp.execFile('echo',['hello','world'],function(err,stdout){
       console.log(stdout);
    });
    

    execFile类似于执行了名为echo的应用,然后传入参数。execFlie会在process.env.PATH的路径中依次寻找是否有名为'echo'的应用,找到后就会执行。默认的process.env.PATH路径中包含了'usr/local/bin',而这个'usr/local/bin'目录中就存在了这个名为'echo'的程序,传入hello和world两个参数,执行后返回。

    (3)安全性分析

    像exec那样,可以直接执行一段shell是极为不安全的,比如有这么一段shell:

    echo hello world;rm -rf
    

    通过exec是可以直接执行的,rm -rf会删除当前目录下的文件。exec正如命令行一样,执行的等级很高,执行后会出现安全性的问题,而execFile不同:

    execFile('echo',['hello','world',';rm -rf'])
    

    在传入参数的同时,会检测传入实参执行的安全性,如果存在安全性问题,会抛出异常。除了execFile外,spawn和fork也都不能直接执行shell,因此安全性较高。

    3、spawn

    spawn同样是用于执行非node应用,且不能直接执行shell,与execFile相比,spawn执行应用后的结果并不是执行完成后,一次性的输出的,而是以流的形式输出。对于大批量的数据输出,通过流的形式可以介绍内存的使用。

    我们用一个文件的排序和去重来举例:

    default

    上述图片示意图中,首先读取的input.txt文件中有acba未经排序的文字,通过sort程序后可以实现排序功能,输出为aabc,最后通过uniq程序可以去重,得到abc。我们可以用spawn流形式的输入输出来实现上述功能:

    let cp=require('child_process');
    let cat=cp.spawn('cat',['input.txt']);
    let sort=cp.spawn('sort');
    let uniq=cp.spawn('uniq');
    
    cat.stdout.pipe(sort.stdin);
    sort.stdout.pipe(uniq.stdin);
    uniq.stdout.pipe(process.stdout);
    console.log(process.stdout);
    

    执行后,最后的结果将输入到process.stdout中。如果input.txt这个文件较大,那么以流的形式输入输出可以明显减小内存的占用,通过设置缓冲区的形式,减小内存占用的同时也可以提高输入输出的效率。

    4、fork

    在javascript中,在处理大量计算的任务方面,HTML里面通过web work来实现,使得任务脱离了主线程。在node中使用了一种内置于父进程和子进程之间的通信来处理该问题,降低了大数据运行的压力。node中提供了fork方法,通过fork方法在单独的进程中执行node程序,并且通过父子间的通信,子进程接受父进程的信息,并将执行后的结果返回给父进程。

    使用fork方法,可以在父进程和子进程之间开放一个IPC通道,使得不同的node进程间可以进行消息通信。

    在子进程中:

    通过process.on('message')和process.send()的机制来接收和发送消息。

    在父进程中:

    通过child.on('message')和process.send()的机制来接收和发送消息。

    具体例子,在child.js中:

    process.on('message',function(msg){
       process.send(msg)
    })
    

    在parent.js中:

    let cp=require('child_process');
    let child=cp.fork('./child');
    child.on('message',function(msg){
      console.log('got a message is',msg);
    });
    child.send('hello world');
    

    执行parent.js会在命令行输出:got a message is hello world

    中断父子间通信的方式,可以通过在父进程中调用:

    child.disconnect()
    

    来实现断开父子间IPC通信。

    5、同步执行的子进程

    exec、execFile、spawn和fork执行的子进程都是默认异步的,子进程的运行不会阻塞主进程。除此之外,child_process模块同样也提供了execFileSync、spawnSync和execSync来实现同步的方式执行子进程。

    三、node中的cluster模块

    cluster意为集成,集成了两个方面,第一个方面就是集成了child_process.fork方法创建node子进程的方式,第二个方面就是集成了根据多核CPU创建子进程后,自动控制负载均衡的方式。

    我们从官网的例子来看:

    const cluster = require('cluster');
    const http = require('http');
    const numCPUs = require('os').cpus().length;
    
    if (cluster.isMaster) {
      console.log(`主进程 ${process.pid} 正在运行`);
    
      // 衍生工作进程。
      for (let i = 0; i < numCPUs; i++) {
        cluster.fork();
      }
    
      cluster.on('exit', (worker, code, signal) => {
        console.log(`工作进程 ${worker.process.pid} 已退出`);
      });
    } else {
      // 工作进程可以共享任何 TCP 连接。
      // 在本例子中,共享的是一个 HTTP 服务器。
      http.createServer((req, res) => {
        res.writeHead(200);
        res.end('你好世界
    ');
      }).listen(8000);
    
      console.log(`工作进程 ${process.pid} 已启动`);
    }
    

    最后输出的结果为:

    $ node server.js
    主进程 3596 正在运行
    工作进程 4324 已启动
    工作进程 4520 已启动
    工作进程 6056 已启动
    工作进程 5644 已启动
    

    我们将master称为主进程,而worker进程称为工作进程,利用cluster模块,使用node封装好的API、IPC通道和调度机可以非常简单的创建包括一个master进程下HTTP代理服务器 + 多个worker进程多个HTTP应用服务器的架构。

    总结

    本文首先介绍了node的单线程和单进程模式,接着从单线程的缺陷触发,介绍了node中如何实现子进程的方法,对比了child_process模块中几种不同的子进程生成方案,最后简单介绍了内置的可以实现子进程以及CPU进程负载均衡的内置集成模块cluster。

  • 相关阅读:
    JVM体系结构之五:本地方法栈
    netty中的ByteBuf
    AtomicLong和LongAdder的区别
    Python介绍
    netty支持的协议
    spring扩展点之四:Spring Aware容器感知技术,BeanNameAware和BeanFactoryAware接口,springboot中的EnvironmentAware
    Netty组件
    《深入Java虚拟机学习笔记》- 第9章 垃圾收集
    《深入Java虚拟机学习笔记》- 第8章 连接模型
    netty中的EventLoop和EventLoopGroup
  • 原文地址:https://www.cnblogs.com/lguow/p/11698625.html
Copyright © 2011-2022 走看看