• 技术文章 >web前端 >js教程

    聊聊Node.js中的多进程和多线程

    青灯夜游青灯夜游2022-07-25 19:45:58转载212
    大家都知道 Node 是单线程的,却不知它也提供了多进(线)程模块来加速处理一些特殊任务,本文便带领大家了解下 Node.js 的多进(线)程,希望对大家有所帮助!

    我们都知道 Node.js 采用的是单线程、基于事件驱动的异步 I/O 模型,其特性决定了它无法利用 CPU 多核的优势,也不善于完成一些非 I/O 类型的操作(比如执行脚本、AI 计算、图像处理等),为了解决此类问题,Node.js 提供了常规的多进(线程)方案(关于进程、线程的讨论,可参见笔者的另一篇文章 Node.js 与并发模型),本文便为大家介绍 Node.js 的多进(线)程机制。

    child_process

    我们可使用 child_process 模块创建 Node.js 的子进程,来完成一些特殊的任务(比如执行脚本),该模块主要提供了 execexecFileforkspwan 等方法,下面我们就简单介绍下这些方法的使用。

    exec

    const { exec } = require('child_process');
    
    exec('ls -al', (error, stdout, stderr) => {
      console.log(stdout);
    });

    该方法根据 options.shell 指定的可执行文件处理命令字符串,在命令的执行过程中缓存其输出,直到命令执行完成后,再将执行结果以回调函数参数的形式返回。

    该方法的参数解释如下:

    execFile

    const { execFile } = require('child_process');
    
    execFile('ls', ['-al'], (error, stdout, stderr) => {
      console.log(stdout);
    });

    该方法的功能类似于 exec,唯一的区别是 execFile 在默认情况下直接用指定的可执行文件(即参数 file 的值)处理命令,这使得其效率略高于 exec(如果查看 shell 的处理逻辑,笔者感觉这效率可忽略不计)。

    该方法的参数解释如下:

    fork

    const { fork } = require('child_process');
    
    const echo = fork('./echo.js', {
      silent: true
    });
    echo.stdout.on('data', (data) => {
      console.log(`stdout: ${data}`);
    });
    
    echo.stderr.on('data', (data) => {
      console.error(`stderr: ${data}`);
    });
    
    echo.on('close', (code) => {
      console.log(`child process exited with code ${code}`);
    });

    该方法用于创建新的 Node.js 实例以执行指定的 Node.js 脚本,与父进程之间以 IPC 方式进行通信。

    该方法的参数解释如下:

    spwan

    const { spawn } = require('child_process');
    
    const ls = spawn('ls', ['-al']);
    ls.stdout.on('data', (data) => {
      console.log(`stdout: ${data}`);
    });
    
    ls.stderr.on('data', (data) => {
      console.error(`stderr: ${data}`);
    });
    
    ls.on('close', (code) => {
      console.log(`child process exited with code ${code}`);
    });

    该方法为 child_process 模块的基础方法,execexecFilefork 最终都会调用 spawn 来创建子进程。

    该方法的参数解释如下:

    小结

    上文对 child_process 模块中主要方法的使用进行了简短介绍,由于 execSyncexecFileSyncforkSyncspwanSync 方法是 execexecFilespwan 的同步版本,其参数并无任何差异,故不再重述。

    cluster

    通过 cluster 模块我们可以创建 Node.js 进程集群,通过 Node.js 进程进群,我们可以更加充分地利用多核的优势,将程序任务分发到不同的进程中以提高程序的执行效率;下面将通过例子为大家介绍 cluster 模块的使用:

    const http = require('http');
    const cluster = require('cluster');
    const numCPUs = require('os').cpus().length;
    
    if (cluster.isPrimary) {
      for (let i = 0; i < numCPUs; i++) {
        cluster.fork();
      }
    } else {
      http.createServer((req, res) => {
        res.writeHead(200);
        res.end(`${process.pid}\n`);
      }).listen(8000);
    }

    上例通过 cluster.isPrimary 属性判断(即判断当前进程是否为主进程)将其分为两个部分:

    运行上面的例子,并在浏览器中访问 http://localhost:8000/,我们会发现每次访问返回的 pid 都不一样,这说明了请求确实被分发到了各个子进程。Node.js 默认采用的负载均衡策略是轮询调度,可通过环境变量 NODE_CLUSTER_SCHED_POLICYcluster.schedulingPolicy 属性来修改其负载均衡策略:

    NODE_CLUSTER_SCHED_POLICY = rr // 或 none
    
    cluster.schedulingPolicy = cluster.SCHED_RR; // 或 cluster.SCHED_NONE

    另外需要注意的是,虽然每个子进程都创建了 HTTP server,并都监听了同一个端口,但并不代表由这些子进程自由竞争用户请求,因为这样无法保证所有子进程的负载达到均衡。所以正确的流程应该是由主进程监听端口,然后将用户请求根据分发策略转发到具体的子进程进行处理。

    由于进程之间是相互隔离的,因此进程之间一般通过共享内存消息传递管道等机制进行通讯。Node.js 则是通过消息传递来完成父子进程之间的通信,比如下面的例子:

    const http = require('http');
    const cluster = require('cluster');
    const numCPUs = require('os').cpus().length;
    
    if (cluster.isPrimary) {
      for (let i = 0; i < numCPUs; i++) {
        const worker = cluster.fork();
        worker.on('message', (message) => {
          console.log(`I am primary(${process.pid}), I got message from worker: "${message}"`);
          worker.send(`Send message to worker`)
        });
      }
    } else {
      process.on('message', (message) => {
        console.log(`I am worker(${process.pid}), I got message from primary: "${message}"`)
      });
      http.createServer((req, res) => {
        res.writeHead(200);
        res.end(`${process.pid}\n`);
        process.send('Send message to primary');
      }).listen(8000);
    }

    运行上面的例子,并访问 http://localhost:8000/,再查看终端,我们会看到类似下面的输出:

    I am primary(44460), I got message from worker: "Send message to primary"
    I am worker(44461), I got message from primary: "Send message to worker"
    I am primary(44460), I got message from worker: "Send message to primary"
    I am worker(44462), I got message from primary: "Send message to worker"

    利用该机制,我们可以监听各子进程的状态,以便在某个子进程出现意外后,能够及时对其进行干预,以保证服务的可用性。

    cluster 模块的接口非常简单,为了节省篇幅,这里只对 cluster.setupPrimary 方法做一些特别声明,其它方法请查看官方文档

    worker_threads

    前文我们对 cluster 模块进行了介绍,通过它我们可以创建 Node.js 进程集群以提高程序的运行效率,但 cluster 基于多进程模型,进程间高成本的切换以及进程间资源的隔离,会随着子进程数量的增加,很容易导致因系统资源紧张而无法响应的问题。为解决此类问题,Node.js 提供了 worker_threads,下面我们通过具体的例子对该模块的使用进行简单介绍:

    // server.js
    const http = require('http');
    const { Worker } = require('worker_threads');
    
    http.createServer((req, res) => {
      const httpWorker = new Worker('./http_worker.js');
      httpWorker.on('message', (result) => {
        res.writeHead(200);
        res.end(`${result}\n`);
      });
      httpWorker.postMessage('Tom');
    }).listen(8000);
    
    // http_worker.js
    const { parentPort } = require('worker_threads');
    
    parentPort.on('message', (name) => {
      parentPort.postMessage(`Welcone ${name}!`);
    });

    上例展示了 worker_threads 的简单使用,在使用 worker_threads 的过程中,需要注意以下几点:

    在 Node.js 中,无论是 cluster 创建的子进程,还是 worker_threads 创建的 Worker 子线程,它们都拥有属于自己的 V8 实例以及事件循环,所不同的是:

    尽管看起来 Worker 子线程比子进程更高效,但 Worker 子线程也有不足的地方,即cluster 提供了负载均衡,而 worker_threads 则需要我们自行完成负载均衡的设计与实现。

    总结

    本文介绍了 Node.js 中 child_processclusterworker_threads 三个模块的使用,通过这三个模块,我们可以充分利用 CPU 多核的优势,并以多进(线)程的模式来高效地解决一些特殊任务(比如 AI、图片处理等)的运行效率。每个模块都有其适用的场景,文中仅对其基本使用进行了说明,如何结合自己的问题进行高效地运用,还需要大家自行摸索。最后,本文若有纰漏之处,还望大家能够指正,祝大家快乐编码每一天。

    更多node相关知识,请访问:nodejs 教程

    以上就是聊聊Node.js中的多进程和多线程的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    上一篇:一文掌握JavaScript树结构深度优先算法 下一篇:深入理解JavaScript内存管理和GC算法
    VIP课程(WEB全栈开发)

    相关文章推荐

    • 【活动】充值PHP中文网VIP即送云服务器• webpack是基于node.js的吗• 深入解析NodeJS中的进程管理• 火了!新的JavaScript运行时:Bun,性能完爆Node• 一文了解Node中的文件模块和核心模块• 聊聊Node.js中的进程、线程、协程与并发模型
    1/1

    PHP中文网