nodejs 多进程和多线程

778 阅读3分钟

nodejs 多进程

为什么要多进程

const http = require('http')
const { fib } = require('./fib')
​
http.createServer((req,res) => {
    console.log(req.url)
    if(req.url === '/fib') {
        res.end(fib(44).toString())
    } else {
        res.end('hellonodejs')
    }
})
.listen(3000)

上面这个node服务,当我们访问http://localhost:3000/fib,因为斐波那契计算非常耗时,此时我们再开一个网页访问此服务下其他地址会因为服务正在计算/fib请求而卡住不能正常响应。 这种场景下可以采取开启nodejs多进程,这样费时请求不会阻塞其他请求的正常响应


child_process 子进程

spawn

child_process.spawn(command[, args][, options])

child_process.spawn() 方法异步衍生子进程,不会阻塞 Node.js 事件循环。 child_process.spawnSync() 函数以同步方式提供等效的功能,其会阻塞事件循环,直到衍生的进程退出或终止

const { spawn } = require("child_process");
const path = require("path");
​
const cp = spawn("node", ["spawn_example.js"], {
  cwd: path.resolve(__dirname, "./"),
  stdio: [0, 1, 2] // [process.stdin, process.stdout, process.stderr]
});
​
cp.on("error", (err) => {
  console.log(err);
});
​
cp.on("exit", (code, signal) => {
  console.log("exit", code, signal); // exit 1 null
});
​
cp.on("close", (code, signal) => {
  console.log("close", code, signal); // close 1 null
});

流方式进程通信

const { spawn } = require('child_process')
const path = require('path') 
​
const cp = spawn("node", ['spawn_example.js'], {
    cwd: path.resolve(__dirname, "./"),
    stdio: ["pipe", "pipe", "pipe"]
})
​
cp.stdout.on("data", (data) => {
    console.log("父进程监听子进程输出: "+data.toString())
})
cp.stdout.write("父进程输出信息...")

ipc方式通信

const { spawn } = require("child_process");
const path = require("path")
​
const cp = spawn("node", ["spawn_example.js"], {
    pwd: ".",
    stdio: [0, 1, 2, "ipc"]
})
​
cp.send("父进程发送的数据")
cp.on("message", (data) => {
    console.log("父进程监听:", data)
})

fork

child_process.fork(modulePath[, args][, options]) child_process.fork() 方法是child_process.spawn() 的特例,专门用于衍生新的 Node.js 进程。 与 child_process.spawn() 一样,返回 ChildProcess 对象。 返回的ChildProcess 将有额外的内置通信通道,允许消息在父进程和子进程之间来回传递

const { fork } = require("child_process")
const path = require("path")
​
const cp = fork("fork_example.js", {
    cwd: path.resolve(__dirname, "fork_test/")
})
​
cp.on("error", err => {
    console.log(err)
})
​
cp.on("exit", (code, signal) => {
    console.log("exit: " + code, signal)
})
​
cp.on("close", (code, signal) => {
    console.log("close: " + code, signal)
})
​
// fork默认使用的ipc
cp.send("子进程发送信息...")
cp.on("message", data => {
    console.log("父进程接收信息:" + data.toString())
})

execFile

child_process.execFile(file[, args][, options][, callback]) child_process.execFile() 函数与 child_process.exec() 类似,不同之处在于它默认不衍生 shell。 而是,指定的可执行文件 file 直接作为新进程衍生,使其比 child_process.exec() 略有效率。 支持与child_process.exec() 相同的选项。 由于未衍生 shell,因此不支持 I/O 重定向和文件通配等行为

const { execFile } = require("child_process");
​
const cp = execFile(
  "node",
  ["execFileWorker.js"],
  {
    cwd: ".",
  },
  (err, stdout, stderr) => {
      console.log(stdout)
  }
);
​
cp.on("error", err => {
    console.log(err)
})

执行脚本

const cp = execFile(
  "node",
  ["--version"],
  {
    cwd: ".",
  },
  (err, stdout, stderr) => {
      console.log("err: " + err)
      console.log("stdout: " + stdout)
      console.log("stderr " + stderr)
  }
);

exec

child_process.exec(command[, options][, callback]) 衍生 shell,然后在该 shell 中执行 command,缓冲任何生成的输出。 传给执行函数的 command 字符串由shell 直接处理,特殊字符(因 shell 而异)需要进行相应处理:

const { exec } = require('child_process');
​
exec('"/path/to/test file/test.sh" arg1 arg2');
// 使用双引号,这样路径中的空格就不会被解释为多个参数的分隔符。
​
exec('echo "The \$HOME variable is $HOME"');
// $HOME 变量在第一个实例中被转义,但在第二个实例中没有。
const { exec } = require('child_process');
exec('cat *.js missing_file | wc -l', (error, stdout, stderr) => {
  if (error) {
    console.error(`exec error: ${error}`);
    return;
  }
  console.log(`stdout: ${stdout}`);
  console.error(`stderr: ${stderr}`);
});

独立子进程

父进程关闭子进程也会关闭,可以通过开启独立子进程使子进程不受父进程控制

const { spawn, fork } = require("child_process");

const cp = spawn("node", ["writeFile.js"], {
  cwd: ".",
  stdio: 'ignore',
  detached: true
});

cp.unref()

cluster集群

Node.js 进程集群可用于运行多个 Node.js 实例,这些实例可以在其应用程序线程之间分配工作负载。 当不需要进程隔离时,请改用 worker_threads 模块,它允许在单个 Node.js 实例中运行多个应用程序线程。

集群模块可以轻松创建共享服务器端口的子进程。

import cluster from 'cluster';
import http from 'http';
import { cpus } from 'os';
import process from 'process';

const numCPUs = cpus().length;

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);

  // 衍生工作进程。
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`worker ${worker.process.pid} died`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接
  // 在本示例中,其是 HTTP 服务器
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end('hello world\n');
  }).listen(8000);

  console.log(`Worker ${process.pid} started`);
}
import cluster from 'cluster';

cluster.setupPrimary({
  exec: 'worker.js',
  args: ['--use', 'https'],
  silent: true
});
cluster.fork(); // https 工作进程
cluster.setupPrimary({
  exec: 'worker.js',
  args: ['--use', 'http']
});
cluster.fork(); // http 工作进程

运行 Node.js 现在将在工作进程之间共享端口 8000:

工作进程使用 child_process.fork() 方法衍生,因此它们可以通过 IPC 与父进程通信并且来回传递服务器句柄。 集群模块支持两种分发传入连接的方法。

第一个(以及除 Windows 之外的所有平台上的默认方法)是循环方法,其中主进程监听端口,接受新连接并以循环方式将它们分发给工作进程,其中一些内置智能以避免工作进程超载。

第二种方法是,主进程创建监听套接字并将其发送给感兴趣的工作进程。 然后工作进程直接接受传入的连接。

理论上,第二种方法具有最好的性能。 但是,在实践中,由于操作系统调度机制难以捉摸,分发往往非常不平衡。 可能会出现八个进程中的两个进程分担了所有连接超过 70% 的负载。

由于 server.listen() 将大部分工作交给了主进程,因此普通的 Node.js 进程和集群工作进程之间的行为在三种情况下会有所不同:

server.listen({fd: 7}) 因为消息传给主进程,所以父进程中的文件描述符 7 将被监听,并将句柄传给工作进程,而不是监听文件描述符 7 引用的工作进程。 server.listen(handle) 显式地监听句柄,将使工作进程使用提供的句柄,而不是与主进程对话。 server.listen(0) 通常,这会使服务器监听随机端口。 但是,在集群中,每个工作进程每次执行 listen(0) 时都会接收到相同的"随机"端口。 实质上,端口第一次是随机的,但之后是可预测的。 要监听唯一的端口,则根据集群工作进程 ID 生成端口号。 Node.js 不提供路由逻辑。 因此,重要的是设计一个应用程序,使其不会过于依赖内存中的数据对象来处理会话和登录等事情。

因为工作进程都是独立的进程,所以它们可以根据程序的需要被杀死或重新衍生,而不会影响其他工作进程。 只要还有工作进程仍然活动,服务器就会继续接受连接。 如果没有工作进程活动,则现有的连接将被丢弃,且新的连接将被拒绝。 但是,Node.js 不会自动管理工作进程的数量。 应用程序有责任根据自己的需要管理工作进程池。

尽管 cluster 模块的主要使用场景是网络,但它也可用于需要工作进程的其他使用场景。

nodejs 多线程

Node.js 通过提供 cluster、child_process API 创建子进程的方式来赋予Node.js “多线程”能力。但是这种创建进程的方式会牺牲共享内存,并且数据通信必须通过json进行传输。(有一定的局限性和性能问题)

基于此 Node.js V10.5.0 提供了 worker_threads,它比 child_process 或 cluster更轻量级。 与child_process 或 cluster 不同,worker_threads 可以共享内存,通过传输 ArrayBuffer 实例或共享 SharedArrayBuffer 实例来实现

Node.js 并没有其它支持多线的程语言(如:java),诸如"synchronized"之类的关键字来实现线程同步的概念。Node.js的 worker_threads 区别于它们的多线程。如果添加线程,语言本身的性质将发生变化,所以不能将线程作为一组新的可用类或函数添加。

我们可以将其理解为:JavaScript和Node.js永远不会有线程,只有基于Node.js 架构的多工作线程

浏览器端: HTML5 制定了 Web Worker 标准(Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行)。

Node端:采用了和 Web Worker相同的思路来解决单线程中大量计算问题 ,官方提供了 child_process 模块和 cluster 模块, cluster 底层是基于child_process实现。

child_process、cluster都是用于创建子进程,然后子进程间通过事件消息来传递结果,这个可以很好地保持应用模型的简单和低依赖。从而解决无法利用多核 CPU 和程序健壮性问题。

Node V10.5.0: 提供了实验性质的 worker_threads模块,才让Node拥有了多工作线程。

Node V12.0.0:worker_threads 已经成为正式标准,可以在生产环境放心使用。

也有很多开发者认为 worker_threads 违背了nodejs设计的初衷,事实上那是它并没有真正理解 worker_threads 的底层原理。其次是每一种语言的出现都有它的历史背景和需要解决的问题,在技术发展的过程中各种语言都是在取长补短,worker_threads 的设计就是技术发展的需要

const {
  Worker, isMainThread, parentPort, workerData
} = require('worker_threads');

if (isMainThread) {
  module.exports = function parseJSAsync(script) {
    return new Promise((resolve, reject) => {
      const worker = new Worker(__filename, {
        workerData: script
      });
      worker.on('message', resolve);
      worker.on('error', reject);
      worker.on('exit', (code) => {
        if (code !== 0)
          reject(new Error(`Worker stopped with exit code ${code}`));
      });
    });
  };
} else {
  const { parse } = require('some-js-parsing-library');
  const script = workerData;
  parentPort.postMessage(parse(script));
}

在构造 worker的时候 传入了一个名为workerData的对象,这是我们希望线程在开始运行时可以访问的数据。

workerData 可以是任何一个JavaScript 值