node中的多进程

javascript 是单线程的并且只在一个进程中跑,

child_process

child_process 模块提供了衍生子进程的能力,此功能主要由 child_process.spawn() 函数提供:

const { spawn } = require("child_process");
const ls = spawn("ls", ["-lh", "/usr"]);

ls.stdout.on("data", (data) => {
  console.log(`stdout: ${data}`);
});
ls.stderr.on("data", (data) => {
  console.error(`stderr: ${data}`);
});
ls.on("close", (code) => {
  console.log(`子进程退出,使用退出码 ${code}`);
});

child_process.spawn() 方法异步地衍生子进程,且不阻塞 Node.js 事件循环。 child_process.spawnSync() 函数则以同步的方式提供了等效的功能,但会阻塞事件循环直到衍生的进程退出或终止。为方便起见, child_process 模块提供了 child_process.spawn() 和 child_process.spawnSync() 的一些同步和异步的替代方法。 这些替代方法中的每一个都是基于 child_process.spawn() 或 child_process.spawnSync() 实现的。

  • [child_process.spawn()]: 方法使用给定的 command 衍生一个新进程,并带上 args 中的命令行参数。 如果省略 args,则其默认为一个空数组。
  • [child_process.exec()]: 衍生一个 shell 然后在该 shell 中执行 command,并缓冲任何产生的输出。
  • [child_process.execFile()]: 类似于 child_process.exec(),但默认情况下不会衍生 shell。 相反,指定的可执行文件 file 会作为新进程直接地衍生,比 child_process.exec() 稍微更高效,由于没有衍生 shell,因此不支持 I/O 重定向和文件通配(模糊搜索文件)等行为。
  • [child_process.fork()]: 衍生一个新的 Node.js 进程,并调用一个指定的模块,该模块已建立了 IPC 通信通道,允许在父进程与子进程之间发送消息。
  • [child_process.execSync()]): [child_process.exec()]的同步版本,将会阻塞 Node.js 事件循环。
  • [child_process.execFileSync()]: [child_process.execFile()]的同步版本,将会阻塞 Node.js 事件循环。

fork 执行程序的示例:

// compute.js
const longComputation = (count) => {
  let sum = 0;
  for (let i = 0; i < count; i++) {
    sum += i;
  }
  return sum;
};

process.on("message", (message) => {
  // 需要先进行序列号
  let count = Number(message);
  const result = longComputation(count);
  process.send(result);
});

// app.js
const compute = fork(path.join(__dirname, "./compute.js"));
compute.send("start");
let asfun = () =>
  new Promise((resolve) => {
    compute.on("message", (result) => {
      resolve(result);
    });
  });
let result = await asfun();

TIPS

fork 衍生的 Node.js 子进程独立于父进程,但两者之间建立的 IPC 通信通道除外。 每个进程都有自己的内存(10m 左右),带有自己的 V8 实例。 由于需要额外的资源分配,因此不建议衍生大量的 Node.js 子进程。

worker_threads

worker_threads 模块允许使用并行执行 JavaScript 的线程,worker_threads 对于执行 CPU 密集型 JavaScript 操作非常有用。他们在 I / O 密集型工作中无济于事。 Node.js 的内置异步 I / O 操作比 Workers 效率更高。 与 child_process 或 cluster 不同,worker_threads 可以共享内存。它们通过传输 ArrayBuffer 实例或共享 SharedArrayBuffer 实例来实现。

// compute.js
const { parentPort } = require("worker_threads");

const longComputation = (count) => {
  let sum = 0;
  for (let i = 0; i < count; i++) {
    sum += i;
  }
  return sum;
};
parentPort.on("message", (msg) => {
  // 不需要先进行序列号
  const result = longComputation(msg);
  parentPort.postMessage({
    [msg]: result,
  });
});

// app.js
const worker = new Worker(path.join(__dirname, "./fork/com_worker.js"), {
  workerData: null,
});
worker.postMessage("msg");
let asfun = () =>
  new Promise((resolve) => {
    worker.on("message", (result) => {
      resolve(result);
    });
  });
let result = await asfun();

上面生成了一个 Worker 线程,通常我们需要创建一个工作者池。否则,创建 Workers 的开销可能会超出其收益

const workerpool = require("workerpool");
const pool = workerpool.pool();

const longComputation = (count) => {
  let sum = 0;
  for (let i = 0; i < count; i++) {
    sum += i;
  }
  return sum;
};

pool.exec(longComputation, [count]);

总结一下

Worker 线程相对于传统的生成子进程的方式更删除与主进程的通信,并且不开启一个 node 实例,占用更小的内存; fork 太多子进程会导致内存爆炸,或者 Too many open files in system(打开的文件/socket 连接数量超过系统设定值的错误)。