Site Overlay

Node中的进程与线程

Node遵循的是单线程单进程的模式,单线程指Js引擎只有一个实例,且在Node的主线程中执行(所以尽量使用异步操作)。好处是维持一个主线程,能减少线程切换的开销。坏处是无法充分利用多核CPU,不能进行CPU密集型操作,否则会阻塞主线程。

CPU密集型操作:大量需要cpu计算的任务。

Node中的进程

Node通过运行node index开启一个服务进程。

const http = require('http');

const server = http.createServer();
server.listen(3000, () => {
  console.log(`进程Id:${process.pid}`); // 35908
});

6867c4c0-819a-4e64-aa4a-16a910099dfe

一个耗时计算的例子

const http = require('http');

const computed = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  }

  return sum;
};

const server = http.createServer((req, res) => {
  if (req.url === '/test'){
    const sum = computed();
    res.end(`sum: ${sum}`);
  } else {
    res.end('index');
  }
});

server.listen(3000);

上面例子中,在进入/test路由时并没有进入页面,而是卡死在那边。由于for循环中的累加大量占用cpu资源,阻塞了主进程。这会导致用户体验非常差,多进程可以解决这个问题,将计算交给另一个进程去做。

Node如何实现多进程

进程之间是通过IPC通讯实现进程之间数据共享的。Node提供了child_process模块来执行可执行文件,调用命令行命令或将.js代码以子进程的方式启动。

IPC通信(全称:InterProcess Communication,进程间通信,通过在内核中开辟一块缓冲区进行交换数据)

child_process模块(四种异步方式)

const {exec, execFile, fork, spawn} = require(‘child_process’);

exec(command[, options][, callback]);
execFile(file[, args][, options][, callback]);
fork(modulePath[, args][, options]);
spawn(command[, args][, options]);

其中exec、execFile、fork 都是 spawn 的延伸应用,底层都是通过 spawn 实现的

方法区别

  • spawn():有三个输入输出流,stdin,stdout,stderr,可实时获取子进程的输入输出和错误信息,通过内置管道与子进程建立IPC通信
  • exec():启动一个子进程来执行命令,调用 bash 来解释命令,将spawn的输入输出流转换成String,默认为utf-8,然后传递给回调函数,获取返回值(默认最大缓存区为200*1024,超出子进程会被kill,该属性也可自行配置)
  • execFile():启动子进程执行可执行文件
  • fork():返回值是childProcess对象,衍生新的进程,进程之间相互独立

解决耗时计算

Fork子进程能充分利用多核CPU,也能解决阻塞主进程的问题。?以上述例子改编

// index.js
const http = require('http');
const { fork } = require('child_process');

const server = http.createServer((req, res) => {
  if (req.url == '/test') {

    const compute = fork('./fork_process.js');
    compute.send('开启一个新的子进程'); // 通过send告诉子进程,自己继续向下执行

    compute.on('message', (sum) => { // 子进程计算完通知父进程,父进程接收
      res.end(`${sum}`);
      compute.kill();
    })

    compute.on('close', () => {
      compute.kill();
    })
    res.write('ok');
  } else {
    res.end('ok');
  }
});

server.listen(3000);
// fork_process.js
const compute = () => {
  let sum = 0;
  for (let i = 0; i < 1e10; i++) {
    sum += i;
  }

  return sum;
}

process.on('message', msg => { // 通过message接收来自父进程的信息,子进程执行计算并将结果返回给父进程
  console.log(msg);
  const sum = compute();
  process.send(sum);
})

这个例子中,当访问/test路由时,页面并没有卡死,页面上展示了’ok’,之后过了几秒再显示子进程最后的计算结果。

cluster模块

在node.js v0.8版本之后有了cluster模块,也可以创建子进程,它是基于child_procee.fork()实现的。
一般来说可以创建的子进程个数是取决于CPU核数的,如果计算机有8个核,我就开一个子进程,还是没有很好的利用多核的好处。
? 本机核数为2~
1d256a8d-f8a0-4893-a673-ca8ed39e6a3a

根据计算机核的个数开子进程

// index.js
const { fork } = require('child_process');
const numCPUs = require('os').cpus().length; // 获取计算机cpu核数,我电脑核就俩

for (let i = 0; i < numCPUs; i += 1) {
  const child = fork('./fork_process.js')
}
// fork_process.js
const http = require('http');

http.createServer((req, res) => {
  res.end('ok');
}).listen(3001)

结果:由于监听同一个端口3001,node报错。。但是改成cluster模块就没有问题了!?

cluster模块例子

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{worker.process.pid} 已退出`);
  });
} else { // 子进程进行服务器创建
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('ok');
  }).listen(8000);
}

白话文解释:可以把父进程执行的部分当做 a.js,子进程执行的部分当做 b.js,你可以把他们想象成是先执行了 node a.js,然后 cluster.fork 了几次,就执行了几次 node b.js 而 cluster 模块则是二者之间的一个桥梁,你可以通过 cluster 提供的方法,让其二者之间进行沟通交流。

为什么它可以这么多进程监听一个端口呢?

# 查看端口信息,发现只有一个进程监听了8000端口
lsof -i:8000

786a1b1b-7130-4317-bfa1-306083148f0d

# 查看进程id的信息
ps -ef | grep 4324

7bd62922-335e-4f59-99e5-a1d4731ea1d7

他们之间的关系是:只有主进程监听了8000端口,由master进程来管理所有的子进程,主进程不负责具体的任务处理,主要工作是负责调度和管理。