首頁  >  文章  >  web前端  >  淺談Nodejs中的多執行緒操作

淺談Nodejs中的多執行緒操作

青灯夜游
青灯夜游轉載
2021-06-23 10:31:354507瀏覽

雖說nodejs是單執行緒的,但它還是容許多執行緒操作,以下這篇文章給大家從 Node 執行緒說起,談談Nodejs中的多執行緒操作,介紹一下worker_threads範本。

淺談Nodejs中的多執行緒操作

本文測試使用環境:
系統:macOS Mojave 10.14.2
CPU:4 核心2.3 GHz
Node: 10.15.1

【推薦學習:《nodejs 教學》】

#從Node 執行緒說起

一般人理解Node是單線程的,所以Node 啟動後線程數應該是1,我們做實驗看一下。 【推薦學習:《nodejs 教學》】

setInterval(() => {
  console.log(new Date().getTime())
}, 3000)

淺談Nodejs中的多執行緒操作

#可以看到 Node 進程佔用了 7 個執行緒。為什麼會有 7 個執行緒呢?

我們都知道,Node 中最核心的是 v8 引擎,在 Node 啟動後,會建立 v8 的實例,這個實例是多執行緒的。

  • 主執行緒:編譯、執行程式碼。
  • 編譯/最佳化執行緒:在主執行緒執行的時候,可以最佳化程式碼。
  • 分析器執行緒:記錄分析程式碼運行時間,為 Crankshaft 最佳化程式碼執行提供依據。
  • 垃圾回收的幾個主題。

所以大家常說的 Node 是單執行緒的指的是 JavaScript 的執行是單執行緒的,但 Javascript 的宿主環境,無論是 Node 或瀏覽器都是多執行緒的。

Node 有兩個編譯器:
full-codegen:簡單快速地將 js 編譯成簡單但是很慢的機械碼。
Crankshaft:比較複雜的即時最佳化編譯器,編譯高效能的可執行程式碼。

某些非同步IO 會佔用額外的執行緒

還是上面那個例子,我們在計時器執行的同時,去讀一個檔案:

const fs = require('fs')

setInterval(() => {
    console.log(new Date().getTime())
}, 3000)

fs.readFile('./index.html', () => {})

淺談Nodejs中的多執行緒操作

線程數量變成了11 個,這是因為在Node 中有一些IO 操作(DNS,FS)和一些CPU 密集計算(Zlib,Crypto)會啟用Node的執行緒池,而執行緒池預設大小為4,因為執行緒數變成了11。

我們可以手動更改執行緒池預設大小:

process.env.UV_THREADPOOL_SIZE = 64

一行程式碼輕鬆把執行緒變成 71。

淺談Nodejs中的多執行緒操作

cluster 是多執行緒嗎?

Node 的單一執行緒也帶來了一些問題,例如對 cpu 利用不足,某個未捕獲的例外可能會導致整個程式的退出等等。因為 Node 中提供了 cluster 模組,cluster 實作了對 child_process 的封裝,透過 fork 方法建立子程序的方式實作了多進程模型。例如我們最常用到的 pm2 就是其中最優秀的代表。

我們看一個cluster 的demo:

const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
  console.log(`主进程 ${process.pid} 正在运行`);
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on(&#39;exit&#39;, (worker, code, signal) => {
    console.log(`工作进程 ${worker.process.pid} 已退出`);
  });
} else {
  // 工作进程可以共享任何 TCP 连接。
  // 在本例子中,共享的是 HTTP 服务器。
  http.createServer((req, res) => {
    res.writeHead(200);
    res.end(&#39;Hello World&#39;);
  }).listen(8000);
  console.log(`工作进程 ${process.pid} 已启动`);
}

這個時候看下活動監視器:

淺談Nodejs中的多執行緒操作

一共有9 個進程,其中一個主進程,cpu 個數x cpu 核數= 2 x 4 = 8 個子進程。

所以無論 child_process 或 cluster,都不是多執行緒模型,而是多行程模型。雖然開發者意識到了單執行緒模型的問題,但沒有從根本上解決問題,而且提供了一個多進程的方式來模擬多執行緒。從前面的實驗可以看出,雖然 Node (V8)本身是具有多線程的能力的,但是開發者並不能很好的利用這個能力,更多的是由 Node 底層提供的一些方式來使用多線程。 Node 官方說:

You can use the built-in Node Worker Pool by developing a C addon. On older versions of Node, build your C addon using NAN, and on newer versions use N-API . node-webworker-threads offers a JavaScript-only way to access Node's Worker Pool.

但是對於JavaScript 開發者,一直沒有一個標準的、好用的方式來使用Node 的多線程能力。

真- Node 多執行緒

直到Node 10.5.0 的發布,官方才給了一個實驗性質的模組worker_threads 到Node提供真正的多線程能力。

先看下簡單的 demo:

const {
  isMainThread,
  parentPort,
  workerData,
  threadId,
  MessageChannel,
  MessagePort,
  Worker
} = require(&#39;worker_threads&#39;);

function mainThread() {
  for (let i = 0; i < 5; i++) {
    const worker = new Worker(__filename, { workerData: i });
    worker.on(&#39;exit&#39;, code => { console.log(`main: worker stopped with exit code ${code}`); });
    worker.on(&#39;message&#39;, msg => {
      console.log(`main: receive ${msg}`);
      worker.postMessage(msg + 1);
    });
  }
}

function workerThread() {
  console.log(`worker: workerDate ${workerData}`);
  parentPort.on(&#39;message&#39;, msg => {
    console.log(`worker: receive ${msg}`);
  }),
  parentPort.postMessage(workerData);
}

if (isMainThread) {
  mainThread();
} else {
  workerThread();
}

上述程式碼在主線程中開啟五個子線程,並且主線程向子線程發送簡單的訊息。

由于 worker_thread 目前仍然处于实验阶段,所以启动时需要增加 --experimental-worker flag,运行后观察活动监视器:

淺談Nodejs中的多執行緒操作

不多不少,正好多了五个子线程。

worker_thread 模块

worker_thread 核心代码

worker_thread 模块中有 4 个对象和 2 个类。

  • isMainThread: 是否是主线程,源码中是通过 threadId === 0 进行判断的。
  • MessagePort: 用于线程之间的通信,继承自 EventEmitter。
  • MessageChannel: 用于创建异步、双向通信的通道实例。
  • threadId: 线程 ID。
  • Worker: 用于在主线程中创建子线程。第一个参数为 filename,表示子线程执行的入口。
  • parentPort: 在 worker 线程里是表示父进程的 MessagePort 类型的对象,在主线程里为 null
  • workerData: 用于在主进程中向子进程传递数据(data 副本)

来看一个进程通信的例子:

const assert = require(&#39;assert&#39;);
const {
  Worker,
  MessageChannel,
  MessagePort,
  isMainThread,
  parentPort
} = require(&#39;worker_threads&#39;);
if (isMainThread) {
  const worker = new Worker(__filename);
  const subChannel = new MessageChannel();
  worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]);
  subChannel.port2.on(&#39;message&#39;, (value) => {
    console.log(&#39;received:&#39;, value);
  });
} else {
  parentPort.once(&#39;message&#39;, (value) => {
    assert(value.hereIsYourPort instanceof MessagePort);
    value.hereIsYourPort.postMessage(&#39;the worker is sending this&#39;);
    value.hereIsYourPort.close();
  });
}

更多详细用法可以查看官方文档

多进程 vs 多线程

根据大学课本上的说法:“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试就够了,但是在实际工作中,我们还是要根据需求合理选择。

下面对比一下多线程与多进程:

属性 多进程 多线程 比较
数据 数据共享复杂,需要用IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,同步复杂 各有千秋
CPU、内存 占用内存多,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 多线程更好
销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 多线程更好
coding 编码简单、调试方便 编码、调试复杂 多进程更好
可靠性 进程独立运行,不会相互影响 线程同呼吸共命运 多进程更好
分布式 可用于多机多核分布式,易于扩展 只能用于多核分布式 多进程更好

上述比较仅表示一般情况,并不绝对。

work_thread 让 Node 有了真正的多线程能力,算是不小的进步。

更多编程相关知识,请访问:编程视频!!

以上是淺談Nodejs中的多執行緒操作的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除