Nodejs child_process

nodejs以单线程模式运行,但使用事件驱动处理并发,有助于创建多个子进程提高性能。
默认nodejs父子进程会建立stdin、stdout、stderr的管道,以非阻塞方式在管道中流通。

child_process

  • child_process.exec(command[. options][, callback])
    使用子进程执行命令,缓存子进程的输出,将子进程的输出以回调函数参数的形式返回。
var exec = require('child_process').exec;

// 成功的例子
exec('ls -al', function(error, stdout, stderr){
    if(error) {
        console.error('error: ' + error);
        return;
    }
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + typeof stderr);
});

// 失败的例子
exec('ls hello.txt', function(error, stdout, stderr){
    if(error) {
        console.error('error: ' + error);
        return;
    }
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + stderr);
});
  • child_process.spawn(command[, args][, options])
    使用指定的命令行参数创建新进程。
  • child_process.fork(modulePath[, args][, options])
    是spawn()的特殊形式,用于在子进程运行的模块,fork('/a.js')相当于`spawn('node, ['/a.js'])。fork会在父子进程间建立通信管道,用于进程间通信。
    每个函数都返回ChildProcess实例,实例实现了Nodejs EventEmitter API,允许父进程注册监听器函数,在子进程生命周期期间,特定的事件发生时调用这些函数。

exec(), execFile(), fork()都是通过spawn()实现的

spawn定义输入输出

const { spawn } = require("child-process");
const path = require("path");

let child = spawn("node", ["sub_process.js", "--port", "3000"], {
    cwd: path.join(__dirname, ""test") // 指定子进程的当前工作目录
    stdio: [0, 1, 2] // 标准输入、标准输出、错误输出
});

// sub_process.js
process.stdout.write(process.argv.toString());

只有输出,没有通信,如果要通信,stdio配置pipe(默认),设置成ignore则输出禁止

// 文件:process.js
const { spawn } = require("child_process");
const path = require("path");
 
// 创建子进程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: ["pipe"]
});
 
child.stdout.on("data", data => console.log(data.toString()));
 
// hello world

// 子进程执行 sub_process.js
process.stdout.write("hello world");

多进程

// 文件:process.js
const { spawn } = require("child_process");
const path = require("path");
 
// 创建子进程
let child1 = spawn("node", ["sub_process_1.js", "--port", "3000"], {
  cwd: path.join(__dirname, "test"),
});
 
let child2 = spawn("node", ["sub_process_2.js"], {
  cwd: path.join(__dirname, "test"),
});

// 读取子进程 1 写入的内容,写入子进程 2
child1.stdout.on("data", data => child2.stdout.write(data.toString));

// 文件:~test/sub_process_1.js
// 获取 --port 和 3000
process.argv.slice(2).forEach(item => process.stdout.write(item));

// 文件:~test/sub_process_2.js
const fs = require("fs");
 
// 读取主进程传递的参数并写入文件
process.stdout.on("data", data => {
  fs.writeFile("param.txt", data, () => {
    process.exit();
  });
});

标准进程通信

// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");
 
// 创建子进程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: [0, "pipe", "ignore", "ipc"]
});
 
child.on("message", data => {
  console.log(data);
 
  // 回复消息给子进程
  child.send("world");
 
  // 杀死子进程
  // process.kill(child.pid);
});
 
// hello

// 文件:~test/sub_process.js
// 给主进程发送消息
process.send("hello");
 
// 接收主进程回复的消息
process.on("message", data => {
  console.log(data);
 
  // 退出子进程
  process.exit();
});
 
// world

这种方式被称为标准进程通信,通过给 options 的 stdio 数组配置 ipc,只要数组中存在 ipc 即可,一般放在数组开头或结尾,配置 ipc 后子进程通过调用自己的 send 方法发送消息给主进程,主进程中用子进程的 message 事件进行接收,也可以在主进程中接收消息的 message 事件的回调当中,通过子进程的 send 回复消息,并在子进程中用 message 事件进行接收,这样的编程方式比较统一,更贴近于开发者的意愿。


退出、杀死子进程

// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");
 
// 创建子进程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: [0, "pipe", "ignore", "ipc"]
});
 
child.on("message", data => {
  console.log(data);
 
  // 杀死子进程
  process.kill(child.pid);
});
 
// hello world

杀死子进程的方法为 process.kill,由于一个主进程可能有多个子进程,所以指定要杀死的子进程需要传入子进程的 pid 属性作为 process.kill 的参数。
注意:退出子进程 process.exit 方法是在子进程中操作的,此时 process 代表子进程,杀死子进程 process.kill 是在主进程中操作的,此时 process 代表主进程。


独立子进程

让子进程不受主进程控制

// 文件:process.js
const { spawn } = require("spawn");
const path = require("path");
 
// 创建子进程
let child = spawn("node", ["sub_process.js"], {
  cwd: path.join(__dirname, "test"),
  stdio: "ignore",
  detached: true
});
 
// 与主进程断绝关系
child.unref();

/ 文件:~test/sub_process.js
const fs = require("fs");
 
setInterval(() => {
  fs.appendFileSync("test.txt", "hello");
});

要想创建的子进程独立,需要在创建子进程时配置 detached 参数为 true,表示该子进程不受控制,还需调用子进程的 unref 方法与主进程断绝关系,但是仅仅这样子进程可能还是会受主进程的影响,要想子进程完全独立需要保证子进程一定不能和主进程共用标准输入、标准输出和错误输出,也就是 stdio 必须设置为 ignore,这也就代表着独立的子进程是不能和主进程进行标准进程通信,即不能设置 ipc。


fork

fork是对spawn的封装

// 文件:process.js
const fork = require("child_process");
const path = require("path");
 
// 创建子进程
let child = fork("sub_process.js", ["--port", "3000"], {
  cwd: path.join(__dirname, "test"),
  silent: true
});
 
child.send("hello world");

// 文件:~test/sub_process.js
// 接收主进程发来的消息
process.on("message", data => console.log(data));

fork 的用法与 spawn 相比有所改变,第一个参数是子进程执行文件的名称,第二个参数为数组,存储执行时的参数和值,第三个参数为 options,其中使用 slilent 属性替代了 spawn 的 stdio,当 silent 为 true 时,此时主进程与子进程的所有非标准通信的操作都不会生效,包括标准输入、标准输出和错误输出,当设为 false 时可正常输出,返回值依然为一个子进程。

fork 创建的子进程可以直接通过 send 方法和监听 message 事件与主进程进行通信。

fork的原理

// 文件:fork.js
const childProcess = require("child_process");
const path = require("path");
 
// 封装原理
childProcess.fork = function (modulePath, args, options) {
  let stdio = options.silent ? ["ignore", "ignore", "ignore", "ipc"] : [0, 1, 2, "ipc"];
  return childProcess.spawn("node", [modulePath, ...args], {
    ...options,
    stdio
  });
}
 
// 创建子进程
let child = fork("sub_process.js", ["--port", "3000"], {
  cwd: path.join(__dirname, "test"),
  silent: false
});
 
// 向子进程发送消息
child.send("hello world");

// 文件:~test/sub_process.js
// 接收主进程发来的消息
process.on("message", data => console.log(data));
 
// hello world

spawn 中的有一些 fork 没有传的参数(如使用 node 执行文件),都在内部调用 spawn 时传递默认值或将默认参数与 fork 传入的参数进行整合,着重处理了 spawn 没有的参数 silent,其实就是处理成了 spawn 的 stdio 参数两种极端的情况(默认使用 ipc 通信),封装 fork 就是让我们能更方便的创建子进程,可以更少的传参。


exec和execFile实现多进程

execFile 和 exec 是 child_process 模块的两个方法,execFile 是基于 spawn 封装的,而 exec 是基于 execFile 封装的,这两个方法用法大同小异,execFile 可以直接创建子进程进行文件操作,而 exec 可以直接开启子进程执行命令,常见的应用场景如 http-server 以及 weboack-dev-server 等命令行工具在启动本地服务时自动打开浏览器。

// execFile 和 exec
const { execFile, exec } = require("child_process");
 
let execFileChild = execFile("node", ["--version"], (err, stdout, stderr) => {
  if (error) throw error;
  console.log(stdout);
  console.log(stderr);
});
 
let execChild = exec("node --version", (err, stdout, stderr) => {
  if (err) throw err;
  console.log(stdout);
  console.log(stderr);
});

cluster

开启进程需要消耗内存,所以开启进程的数量要适合,合理运用多进程可以大大提高效率,如 Webpack 对资源进行打包,就开启了多个进程同时进行,大大提高了打包速度,集群也是多进程重要的应用之一,用多个进程同时监听同一个服务,一般开启进程的数量跟 CPU 核数相同为好,此时多个进程监听的服务会根据请求压力分流处理,也可以通过设置每个子进程处理请求的数量来实现 “负载均衡”。

使用ipc实现集群

ipc 标准进程通信使用 send 方法发送消息时第二个参数支持传入一个服务,必须是 http 服务或者 tcp 服务,子进程通过 message 事件进行接收,回调的参数分别对应发送的参数,即第一个参数为消息,第二个参数为服务,我们就可以在子进程创建服务并对主进程的服务进行监听和操作(listen 除了可以监听端口号也可以监听服务),便实现了集群,代码如下。

// 文件:server.js
const os = require("os"); // os 模块用于获取系统信息
const http = require("http");
const path = require("path");
const { fork } = require("child_process");
 
// 创建服务
const server = http.createServer((res, req) => {
  res.end("hello");
}).listen(3000);
 
// 根据 CPU 个数创建子进程
os.cpus().forEach(() => {
  fork("child_server.js", {
    cwd: path.join(__dirname);
  }).send("server", server);
});

// 文件:child_server.js
const http = require("http");
 
// 接收来自主进程发来的服务
process.on("message", (data, server) => {
process.stdout.write(`child${process.pid}`);
  http.createServer((req, res) => {
    res.end(`child${process.pid}`);
  }).listen(server); // 子进程共用主进程的服务
});

使用cluster实现集群

cluster 模块是 NodeJS 提供的用来实现集群的,他将 child_process 创建子进程的方法集成进去,实现方式要比使用 ipc 更简洁。

// 文件:cluster.js
const cluster = require("cluster");
const http = require("http");
const os = require("os");
 
// 判断当前执行的进程是否为主进程,为主进程则创建子进程,否则用子进程监听服务
if (cluster.isMaster) {
  // 创建子进程
  os.cpus().forEach(() => cluster.fork());
} else {
  // 创建并监听服务
  http.createServer((req, res) => {
    res.end(`child${process.pid}`);
  }).listen(3000);
}

上面代码既会执行 if 又会执行 else,这看似很奇怪,但其实不是在同一次执行的,主进程执行时会通过 cluster.fork 创建子进程,当子进程被创建会将该文件再次执行,此时则会执行 else 中对服务的监听,还有另一种用法将主进程和子进程执行的代码拆分开,逻辑更清晰,用法如下。

// 文件:cluster.js
const cluster = require("cluster");
const path = require("path");
const os = require("os");
 
// 设置子进程读取文件的路径
cluster.setupMaster({
  exec: path.join(__dirname, "cluster-server.js")
});
 
// 创建子进程
os.cpus().forEach(() => cluster.fork());

/// 文件:cluster-server.js
const http = require("http");
 
// 创建并监听服务
http.createServer((req, res) => {
  res.end(`child${process.pid}`);
}).listen(3000);

通过 cluster.setupMaster 设置子进程执行文件以后,就可以将主进程和子进程的逻辑拆分开,在实际的开发中这样的方式也是最常用的,耦合度低,可读性好,更符合开发的原则。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容