node创建子进程

207 阅读3分钟

进程和线程

  • 一个进程可以包含多个线程
  • 多核cpu可以启动多个进程去监听服务
  • node中主线程是单线程,而且它不支持开启子线程
    • 那么当同步任务阻塞,会影响到其他的请求
    • 也可以开启子进程,那么需要做一件事,当前的一段逻辑交给子进程处理了,它做完需要告知我(发布订阅)
    • 不要滥用进程

阻塞示例

// 先访问/sum,再访问任意页面都会阻塞
// 会等sum处理完成,再处理其他页面
// 哪怕把/sum放在setTimeout里也不好使
const http = require("http");

http
  .createServer((req, res) => {
    if (req.url === "/sum") {
      let sum = 0;
      for (let i = 0; i < 100 * 10000 * 10000; i++) {
        sum += i;
      }
      return res.end("sun:" + sum);
    } else {
      return res.end("ok");
    }
  })
  .listen(3000);

如何开启子进程

  • 核心模块 child_process

api

spawn

  • spawn(执行环境, [执行文件], {...配置})
  • 返回一个实例
const path = require("path");
const { spawn } = require("child_process");

// 使用node执行当前目录下的index.js
const cp = spawn("node", ["index.js"], {
  cwd: path.resolve(__dirname),
});

// 监听子进程执行结束
cp.stdout.on("exit", (chunk) => {
  console.log('执行结束');
});
//监听子进程关闭
cp.stderr.on("close", (chunk) => {
  console.log('关闭');
});

options.stdio
  • 很重要的一个参数
  • 默认为 pipe
  • 格式有两种
    • string
    • 为Array,放3个值
  • 'pipe' = ['pipe','pipe','pipe']
    • 字面上的意思,将输入输出变为流的形式
    • 数组中的三个pipe分别代表 stdin、stdout、stderr,使用这三个都可以监听到
    • 不过它是将这些内容写到自己的process上,所以需要通过实例(cp)去监听
    • 我们可以通过对这三个参数的监听和写入实现通信
// process.stdin  0  标准输入
// process.stdout 1  标准输出
// process.stderr 2  错误输出

// err.js
process.stdout.write("测试是否接收到");
// console.log('xxx') 使用console.log也可以,不过它会有空行,会默认换行

// index.js 执行node index.js
const cp = spawn("node", ["err.js"], { stdio: "pipe" });

cp.stdout.on("data", (chunk) => {
  console.log(chunk.toString());// 测试是否接收到
});
  • 测试异常
// err.js
throw 1;
// index.js
cp.stderr.on("data", (chunk) => {
  // node会把异常信息自动写入stderr中,那么我们就可以根据data时间获取
  console.log(`异常:${chunk.toString()}`);// 异常:xxxxx
});
  • 写入
    • 写入后是不会自动断开连接的
//err.js
process.stdin.on("data", (chunk) => {
  console.log("子拿到的啥啊", chunk.toString());
});
console.log(1);
// index.js
cp.stdin.write("内容");
cp.stdout.on("data", (chunk) => {
  console.log("父" + chunk.toString());
});
  • 'inherit' = [0,1,2]
    • 等同与[process.stdin, process.stdout, process.stderr]
    • 这种用法是将子进程与当前进程的输入输出共享,所以也就不需要监听了,子进程的内容会直接再当前命令窗口输出
const cp = spawn("node", ["err.js"], {
  stdio: [0, 1, 2],
});
// 不需要了
// cp.stderr.on
  • 'ignore'
    • 表示不需要关注子进程的输出
  • 使用pipe的通信稍微有一些麻烦,所以可以使用ipc通信
    • 在数组末尾添加ipc [0,1,2,'ipc']
    • 通信使用send,监听使用message事件
    • 传递的不是buffer了,不需要转换字符
    • 输入输出共享了
    • 不会自动关闭,所以手动kill下
// 子
process.on("message", (data) => {
  console.log("父pid" + data);
  process.kill(process.pid); // 杀掉自己
});
process.send(process.pid, () => {
  console.log("子发送成功");
});

// 父
const cp = spawn("node", ["err.js"], {
  stdio: [0, 1, 2, "ipc"],
});

cp.on("message", (data) => {
  console.log("子进程pid" + data);
  cp.send(process.pid, () => {
    console.log("父发送成功");
  });
});
options.detached
  • 默认false
  • true为告诉子进程,让其独立执行
  • 当为false的时候会与父进程关联,那么父进程挂啦,子进程也就挂了
  • 适用于一些定期执行的任务,如定期清理日志
  • 需要与stdio:'ignore'和cp.unref()联合使用,否则不生效
spawn("node", ["err.js"], {
  stdio: "ignore",
  detached: true,//告诉子进程独立
});
cp.unref();// 断开与子进程连接
fork
  • 是ipc方法的封装,还是基于spawn
    • 使用node执行,所以第一个参数可以忽略
    • options.stdio就是ipc的参数
...
// 将声明换为fork即可
const cp = fork(["err.js"]);
...
execFile
  • 适用于执行某个小文件
  • 接收一个回调,回调形参依次是err、stdout、stderr
const { execFile } = require("child_process");
execFile("node", ["cpu.js"], {}, function (err, stdout, stderr) {
  console.log(stdout);
});
exec
  • 适用于执行一个命令
  • 可以理解为将execFile的前两个参数合并,后续参数一致
  • execFile有一定的局限性,如获取变量echo $PATH,在exec中可以直接合并,而在execFile需要分开写,那么拿到的就是个固定字符
execFile("echo", ["$PATH"], {}, function (err, stdout, stderr) {
  console.log(stdout);//$PATH
});

exec("echo $PATH", {}, function (err, stdout, stderr) {
  console.log(stdout);///Users/......
});
适用场景
  • 获取大文件pipe
  • 小文件或通信 ipc
  • 获取固定结果 exec或execFile

解决开始的问题

  • 开启多个子进程(最好为主机核数-1),监听同一个端口号
  • 当请求过来后,会自动识别当前进程是否被占用,如果占用,就找另外的进程
  • 当所有进程都卡住,仍然会卡住,不过已经在一定程度上解决了这个问题
  • 想彻底解决的话,还是需要避免这种阻塞
// 主进程
const http = require("http");
const cpus = require("os").cpus();
const { fork } = require("child_process");
const server = http
  .createServer((req, res) => {})
  .listen(3000);

for (let i = 0, len = cpus.length - 1; i < len; i++) {
  let cp = fork("http.js");
  // send方法的第二个参数可以传递一个服务器
  cp.send("server", server);
}

// 子进程,http.js
const http = require("http");
process.on("message", (type, server) => {
  http
    .createServer((req, res) => {
      console.log(process.pid, "子进程pid");
      if (req.url === "/sum") {
        let sum = 0;
        for (let i = 0; i < 100 * 10000 * 10000; i++) {
          sum += i;
        }
        return res.end("sun:" + sum);
      } else {
        return res.end("ok");
      }
    })
    .listen(server);
});

使用自带库

  • cluster
  • node版本要高一点,我用的16
  • 首次进入isPrimary为true,代表主进程,调用fork后,再进入就为false,代表子进程
const cluster = require("cluster");
const http = require("http");
const cpus = require("os").cpus();
console.log(cluster.isPrimary);
if (cluster.isPrimary) {
  for (let i = 0, len = cpus.length - 1; i < len; i++) {
    // 执行fork会自动再次执行本文件,但这时候cluster.isPrimary已经为false,代表进入子进程
    cluster.fork();
  }
} else {
  http
    .createServer((req, res) => {
      console.log(process.pid, "子进程pid");
      if (req.url === "/sum") {
        let sum = 0;
        for (let i = 0; i < 100 * 10000 * 10000; i++) {
          sum += i;
        }
        return res.end("sun:" + sum);
      } else {
        return res.end("ok");
      }
    })
    .listen(3000, () => {
    // 这里会自己默认会识别端口情况,会自动将server传递过去,防止报错
      console.log("创建成功", process.pid);
    });
}