“进程” 是计算机系统进行资源分配和调度的基本单位,咱们能够理解为计算机每开启一个任务就会建立至少一个进程来处理,有时会建立多个,如 Chrome 浏览器的选项卡,其目的是为了防止一个进程挂掉而应用中止工做,而 “线程” 是程序执行流的最小单元,NodeJS 默认是单进程、单线程的,咱们将这个进程称为主进程,也能够经过 child_process
模块建立子进程实现多进程,咱们称这些子进程为 “工做进程”,而且归主进程管理,进程之间默认是不能通讯的,且全部子进程执行任务都是异步的。html
在 NodeJS 中执行一个 JS 文件,若是想在这个文件中再同时(异步)执行另外一个 JS 文件,可使用 child_process
模块中的 spawn
来实现,spawn
能够帮助咱们建立一个子进程,用法以下。node
// 文件:process.js const { spawn } = require("child_process"); const path = require("path"); // 建立子进程 let child = spawn("node", ["sub_process.js", "--port", "3000"], { cwd: path.join(__dirname, "test") // 指定子进程的当前工做目录 }); // 出现错误触发 child.on("error", err => console.log(err)); // 子进程退出触发 child.on("exit", () => console.log("exit")); // 子进程关闭触发 child.on("close", () => console.log("close")); // exit // close
spawn
方法能够帮助咱们建立一个子进程,这个子进程就是方法的返回值,spawn
接收如下几个参数:web
error
事件在子进程出错时触发,exit
事件在子进程退出时触发,close
事件在子进程关闭后触发,在子进程任务结束后 exit
必定会触发,close
不必定触发。编程
// 文件:~test/sub_process.js // 打印子进程执行 sub_process.js 文件的参数 console.log(process.argv);
经过上面代码打印了子进程执行时的参数,可是咱们发现主进程窗口并无打印,咱们但愿的是子进程的信息能够反馈给主进程,要实现通讯须要在建立子进程时在第三个参数 options
中配置 stdio
属性定义。api
// 文件:process.js const { spawn } = require("child_process"); const path = require("path"); // 建立子进程 let child = spawn("node", ["sub_process.js", "--port", "3000"], { cwd: path.join(__dirname, "test") // 指定子进程的当前工做目录 // stdin: [process.stdin, process.stdout, process.stderr] stdio: [0, 1, 2] // 配置标准输入、标准输出、错误输出 }); // C:\Program Files\nodejs\node.exe,g:\process\test\sub_process.js,--port,3000
// 文件:~test/sub_process.js // 使用主进程的标准输出,输出 sub_process.js 文件执行的参数 process.stdout.write(process.argv.toString());
经过上面配置 options
的 stdio
值为数组,上面的两种写法做用相同,都表示子进程和主进程共用了主进程的标准输入、标准输出、和错误输出,实际上并无实现主进程与子进程的通讯,其中 0
和 stdin
表明标准输入,1
和 stdout
表明标准输出,2
和 stderr
表明错误输出。数组
上面这样的方式只要子进程执行 sub_process.js
就会在窗口输出,若是咱们但愿是否输出在主进程里面控制,即实现子进程与主进程的通讯,看下面用法。浏览器
// 文件: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
// 文件:~test/sub_process.js // 子进程执行 sub_process.js process.stdout.write("hello world");
上面将 stdio
内数组的值配置为 pipe
(默认不写就是 pipe
),则经过流的方式实现主进程和子进程的通讯,经过子进程的标准输出(可写流)写入,在主进程经过子进程的标准输出经过 data
事件读取的流在输出到窗口(这种写法不多用),上面都只在主进程中开启了一个子进程,下面举一个开启多个进程的例子。app
例子的场景是主进程开启两个子进程,先运行子进程 1
传递一些参数,子进程 1
将参数取出返还给主进程,主进程再把参数传递给子进程 2
,经过子进程 2
将参数写入到文件 param.txt
中,这个过程不表明真实应用场景,主要目的是体会主进程和子进程的通讯过程。负载均衡
// 文件: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(); }); });
有一点须要注意,在子进程 2
写入文件的时候,因为主进程不知道子进程 2
何时写完,因此主进程会卡住,须要子进程在写入完成后调用 process.exit
方法退出子进程,子进程退出并关闭后,主进程会随之关闭。异步
在咱们给 options
配置 stdio
时,数组内其实能够对标准输入、标准输出和错误输出分开配置,默认数组内为 pipe
时表明三者都为 pipe
,分别配置看下面案例。
// 文件: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", 2] }); // world
// 文件:~test/sub_process.js console.log("hello"); console.error("world");
上面代码中对 stderr
实现了默认打印而不通讯,对标准输入实现了通讯,还有一种状况,若是但愿子进程只是默默的执行任务,而在主进程命令窗口什么类型的输出都禁止,能够在数组中对应位置给定值 ignore
,将上面案例修改以下。
// 文件: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"] });
// 文件:~test/sub_process.js console.log("hello"); console.error("world");
此次咱们发现不管标准输出和错误输出都没有生效,上面这些方式实际上是不太方便的,由于输出有 stdout
和 stderr
,在写法上没办法统一,能够经过下面的方式来统一。
// 文件: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
// 文件:~test/sub_process.js // 给主进程发送消息 process.send("hello");
从上面代码咱们能够看出,杀死子进程的方法为 process.kill
,因为一个主进程可能有多个子进程,因此指定要杀死的子进程须要传入子进程的 pid
属性做为 process.kill
的参数。
{% note warning %}
注意:退出子进程 process.exit
方法是在子进程中操做的,此时 process
表明子进程,杀死子进程 process.kill
是在主进程中操做的,此时 process
表明主进程。
{% endnote %}
咱们前面说过,child_process
模块建立的子进程是被主进程统一管理的,若是主进程挂了,全部的子进程也会受到影响一块儿挂掉,但其实使用多进程一方面为了提升处理任务的效率,另外一方面也是为了当一个进程挂掉时还有其余进程能够继续工做,不至于整个应用挂掉,这样的例子很是多,好比 Chrome 浏览器的选项卡,好比 VSCode 编辑器运行时都会同时开启多个进程同时处理任务,其实在 spawn
建立子进程时,也能够实现子进程的独立,即子进程再也不受主进程的控制和影响。
// 文件: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
也是 child_process
模块的一个方法,与 spawn
相似,是在 spawn
的基础上又作了一层封装,咱们看一个 fork
使用的例子。
// 文件: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
的原理很是简单,只是在子进程模块 child_process
上挂了一个 fork
方法,而在该方法内调用 spawn
并将 spawn
返回的子进程做为返回值返回,下面进行简易实现。
// 文件: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
就是让咱们能更方便的建立子进程,能够更少的传参。
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); });
exec
与 execFile
的区别在于传参,execFile
第一个参数为文件的可执行路径或命令,第二个参数为命令的参数集合(数组),第三个参数为 options
,最后一个参数为回调函数,回调函数的形参为错误、标准输出和错误输出。
exec
在传参上将 execFile
的前两个参数进行了整合,也就是命令与命令参数拼接成字符串做为第一参数,后面的参数都与 execFile
相同。
开启进程须要消耗内存,因此开启进程的数量要适合,合理运用多进程能够大大提升效率,如 Webpack 对资源进行打包,就开启了多个进程同时进行,大大提升了打包速度,集群也是多进程重要的应用之一,用多个进程同时监听同一个服务,通常开启进程的数量跟 CPU 核数相同为好,此时多个进程监听的服务会根据请求压力分流处理,也能够经过设置每一个子进程处理请求的数量来实现 “负载均衡”。
ipc
标准进程通讯使用 send
方法发送消息时第二个参数支持传入一个服务,必须是 http
服务或者 tcp
服务,子进程经过 message
事件进行接收,回调的参数分别对应发送的参数,即第一个参数为消息,第二个参数为服务,咱们就能够在子进程建立服务并对主进程的服务进行监听和操做(listen
除了能够监听端口号也能够监听服务),便实现了集群,代码以下。
// 文件:server.js const os = require("os"); // os 模块用于获取系统信息 const http = require("http"); const path = require("path"); const { fork } = rquire("child_process"); // 建立服务 const server = 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) => { http.createServer((req, res) => { res.end(`child${process.pid}`); }).listen(server); // 子进程共用主进程的服务 });
上面代码中由主进程处理的请求会返回 hello
,由子进程处理的请求会返回 child
加进程的 pid
组成的字符串。
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
设置子进程执行文件之后,就能够将主进程和子进程的逻辑拆分开,在实际的开发中这样的方式也是最经常使用的,耦合度低,可读性好,更符合开发的原则。
本篇着重的介绍了 NodeJS 多进程的实现方式以及集群的使用,之因此在开头长篇大论的介绍 spawn
,是由于其余的全部跟多进程相关的方法包括 fork
、exec
等,以及模块 cluster
都是基于 spawn
的封装,若是对 spawn
足够了解,其余的也不在话下,但愿你们经过这篇能够在 NodeJS 多进程相关的开发中起到一个 “路标” 的做用。