Node.js 多進程處理CPU密集任務

Node.js 單線程與多進程

你們都知道 Node.js 性能很高,是以異步事件驅動、非阻塞 I/O 而被普遍使用。但缺點也很明顯,因爲 Node.js 是單線程程序,若是長時間運算,會致使 CPU 不能及時釋放,因此並不適合 CPU 密集型應用。node

固然,也不是沒有辦法解決這個問題。雖然 Node.js 不支持多線程,可是可建立多子進程來執行任務。
Node.js 提供了 child_processcluster 兩個模塊可用於建立多子進程多線程

下面咱們就分別使用單線程和多進程來模擬查找大量斐波那契數進行 CPU 密集測試異步

如下代碼是查找 500 次位置爲 35 的斐波那契數(方便測試,定了一個時間不須要太長也不會過短的位置)函數

單線程處理

代碼:single.js

function fibonacci(n) {
  if (n == 0 || n == 1) {
    return n;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

let startTime = Date.now();
let totalCount = 500;
let completedCount = 0;
let n = 35;

for (let i = 0; i < totalCount; i++) {
  fibonacci(n);
  completedCount++;
  console.log(`process: ${completedCount}/${totalCount}`);
}
console.log("👏 👏 👏 👏 👏 👏 👏 👏 👏 👏");
console.info(`任務完成,用時: ${Date.now() - startTime}ms`);
console.log("👏 👏 👏 👏 👏 👏 👏 👏 👏 👏");

執行node single.js 查看結果性能

在個人電腦上顯示結果爲44611ms(電腦配置不一樣也會有差別)。測試

...
process: 500/500
👏 👏 👏 👏 👏 👏 👏 👏 👏 👏
任務完成,用時: 44611ms
👏 👏 👏 👏 👏 👏 👏 👏 👏 👏

查找 500 次須要 44 秒,太慢了。可想而知若是位置更大,數量更多...ui

那咱們來嘗試用多進程試試 ⬇️this

多進程

採用 cluster 模塊, Master-Worker 模式來測試
共 3 個 js,分別爲主線程代碼: master.js、子進程代碼: worker.js、入口代碼: cluster.js(入口可無需單獨寫一個 js、這裏是爲了看起來更清楚一些)

主線程代碼:master.js

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

// 設置子進程執行程序
cluster.setupMaster({
  exec: "./worker.js",
  slient: true
});

function run() {
  // 記錄開始時間
  const startTime = Date.now();
  // 總數
  const totalCount = 500;
  // 當前已處理任務數
  let completedCount = 0;
  // 任務生成器
  const fbGenerator = FbGenerator(totalCount);

  if (cluster.isMaster) {
    cluster.on("fork", function(worker) {
      console.log(`[master] : fork worker ${worker.id}`);
    });
    cluster.on("exit", function(worker, code, signal) {
      console.log(`[master] : worker ${worker.id} died`);
    });

    for (let i = 0; i < numCPUs; i++) {
      const worker = cluster.fork();

      // 接收子進程數據
      worker.on("message", function(msg) {
        // 完成一個,記錄並打印進度
        completedCount++;
        console.log(`process: ${completedCount}/${totalCount}`);

        nextTask(this);
      });

      nextTask(worker);
    }
  } else {
    process.on("message", function(msg) {
      console.log(msg);
    });
  }

  /**
   * 繼續下一個任務
   *
   * @param {ChildProcess} worker 子進程對象,將在此進程上執行本次任務
   */
  function nextTask(worker) {
    // 獲取下一個參數
    const data = fbGenerator.next();
    // 判斷是否已經完成,若是完成則調用完成函數,結束程序
    if (data.done) {
      done();
      return;
    }
    // 不然繼續任務
    // 向子進程發送數據
    worker.send(data.value);
  }

  /**
   * 完成,當全部任務完成時調用該函數以結束程序
   */
  function done() {
    if (completedCount >= totalCount) {
      cluster.disconnect();
      console.log("👏 👏 👏 👏 👏 👏 👏 👏 👏 👏");
      console.info(`任務完成,用時: ${Date.now() - startTime}ms`);
      console.log("👏 👏 👏 👏 👏 👏 👏 👏 👏 👏");
    }
  }
}

/**
 * 生成器
 */
function* FbGenerator(count) {
  var n = 35;
  for (var i = 0; i < count; i++) {
    yield n;
  }
  return;
}

module.exports = {
  run
};
1.這裏是根據當前電腦的邏輯 CPU 核數來建立子進程的,不一樣電腦數量也會不同,個人 CPU 是 6 個物理核數,因爲支持超線程處理,因此邏輯核數是 12,故會建立出 12 個子進程

2.主線程與子進程之間通訊是經過send方法來發送數據,監聽message事件來接收數據線程

3.不知道你們有沒有注意到我這裏使用了 ES6 的 Generator 生成器來模擬生成每次須要查找的斐波那契數位置(雖然是寫死的 😂,爲了和上面的單線程保證統一)。這麼作是爲了避免讓全部任務一次性扔出去,由於就算扔出去也會被阻塞,還不如放在程序端就給控制住,完成一個,放一個。code

子進程代碼:worker.js

function fibonacci(n) {
  if (n == 0 || n == 1) {
    return n;
  } else {
    return fibonacci(n - 1) + fibonacci(n - 2);
  }
}

// 接收主線程發送過來的任務,並開始查找斐波那契數
process.on("message", n => {
  var res = fibonacci(n);
  // 查找結束後通知主線程,以便主線程再度進行任務分配
  process.send(res);
});

入口代碼:cluster.js

// 引入主線程js,並執行暴露出來的run方法
const master = require("./master");
master.run();

執行node cluster.js 查看結果

在個人電腦上顯示結果爲10724ms(電腦配置不一樣也會有差別)。

process: 500/500
👏 👏 👏 👏 👏 👏 👏 👏 👏 👏
任務完成,用時: 10724ms
👏 👏 👏 👏 👏 👏 👏 👏 👏 👏

結果

進過上面兩種方式的對比,結果很明顯,多進程處理速度是單線程處理速度的 4 倍多。並且有條件的狀況下,若是電腦 CPU 足夠,進程數更多,那麼速度也會更快。

若是有更好的方案或別的語言能處理你的需求那就更好,誰讓 Node.js 天生就不適合 CPU 密集型應用呢。。

閱讀原文
相關文章
相關標籤/搜索