Node.js中的進程與線程

2022-01-27 騰訊IMWeb前端團隊
1. 回顧進程和線程的定義

進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位。

線程(Thread)是作業系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。

2. Node.js的單線程

Node特點主線程是單線程的 一個進程只開一個主線程,基於事件驅動的、異步非阻塞I/O,可以應用於高並發場景。

Nodejs中沒有多線程,為了充分利用多核cpu,可以使用子進程實現內核的負載均衡,那我們就要解決以下問題:

3. 場景實例

const http = require('http');
http.createServer((req,res)=>{
    if(req.url === '/sum'){ // 求和
        let sum = 0;
        for(let i = 0 ; i < 10000000000 ;i++){
            sum+=i;
        }
        res.end(sum+'')
    }else{
        res.end('end');
    }
}).listen(3000);
// 這裡我們先訪問/sum,在新建一個瀏覽器頁卡訪問/ 
// 會發現要等待/sum路徑處理後才能處理/路徑 

4. 開啟進程

Node.js 進程創建,是通過child_process模塊實現的:

child_process.spawn() 異步生成子進程。

child_process.fork() 產生一個新的Node.js進程,並使用建立的IPC通信通道調用指定的模塊,該通道允許在父級和子級之間發送消息。

child_process.exec() 產生一個shell並在該shell中運行命令。

child_process.execFile() 無需產生shell。

4.1. spawn

spawn產卵,可以通過此方法創建一個子進程:

let { spawn } = require("child_process");
let path = require("path");
// 通過node命令執行sub_process.js文件
let childProcess = spawn("node",['sub_process.js'], {
  cwd: path.resolve(__dirname, "test"), // 找文件的目錄是test目錄下
  stdio: [0, 1, 2] 
});
// 監控錯誤
childProcess.on("error", function(err) {
  console.log(err);
});
// 監聽關閉事件
childProcess.on("close", function() {
  console.log("close");
});
// 監聽退出事件
childProcess.on("exit", function() {
  console.log("exit");
});

stido這個屬性非常有特色,這裡我們給了0,1,2這三個值分別對應住進程的process.stdin,process.stdout和process.stderr這代表著主進程和子進程共享標準輸入和輸出:

let childProcess = spawn("node",['sub_process.js'], {
  cwd: path.resolve(__dirname, "test"), // 找文件的目錄是test目錄下
  stdio: [0, 1, 2] 
});

可以在當前進程下列印sub_process.js執行結果默認在不提供stdio參數時為stdio:['pipe'],也就是只能通過流的方式實現進程之間的通信:

let { spawn } = require("child_process");
let path = require("path");
// 通過node命令執行sub_process.js文件
let childProcess = spawn("node",['sub_process.js'], {
  cwd: path.resolve(__dirname, "test"),
  stdio:['pipe'] // 通過流的方式
});
// 子進程讀取寫入的數據
childProcess.stdout.on('data',function(data){
    console.log(data);
});
// 子進程像標準輸出中寫入
process.stdout.write('hello');

使用ipc方式通信,設置值為stdio:['pipe','pipe','pipe','ipc']可以通過on('message')和send方式進行通信:

let { spawn } = require("child_process");
let path = require("path");
// 通過node命令執行sub_process.js文件
let childProcess = spawn("node",['sub_process.js'], {
  cwd: path.resolve(__dirname, "test"),
  stdio:['pipe','pipe','pipe','ipc'] // 通過流的方式
});
// 監聽消息
childProcess.on('message',function(data){
    console.log(data);
});
// 發送消息
process.send('hello');

還可以傳入ignore進行忽略,傳入inherit表示默認共享父進程的標準輸入和輸出。

產生獨立進程:

let { spawn } = require("child_process");
let path = require("path");
// 通過node命令執行sub_process.js文件
let child = spawn('node',['sub_process.js'],{
    cwd:path.resolve(__dirname,'test'),
    stdio: 'ignore',
    detached:true // 獨立的線程
});
child.unref(); // 放棄控制

4.2. fork

衍生新的進程,默認就可以通過ipc方式進行通信:

let { fork } = require("child_process");
let path = require("path");
// 通過node命令執行sub_process.js文件
let childProcess = fork('sub_process.js', {
  cwd: path.resolve(__dirname, "test"),
});
childProcess.on('message',function(data){
    console.log(data);
});

fork是基於spawn的,可以多傳入一個silent屬性來設置是否共享輸入和輸出。

fork原理:

function fork(filename,options){
    let stdio = ['inherit','inherit','inherit']
    if(options.silent){ // 如果是安靜的  就忽略子進程的輸入和輸出
        stdio = ['ignore','ignore','ignore']
    }
    stdio.push('ipc'); // 默認支持ipc的方式
    options.stdio = stdio
    return spawn('node',[filename],options)
}

到了這裡我們就可以解決「3.場景實例」中的場景實例了:

const http = require('http');
const {fork} = require('child_process');
const path = require('path');
http.createServer((req,res)=>{
    if(req.url === '/sum'){
        let childProcess = fork('calc.js',{
            cwd:path.resolve(__dirname,'test')
        });
        childProcess.on('message',function(data){
            res.end(data+'');
        })
    }else{
        res.end('ok');
    }
}).listen(3000);

4.3. execFile

通過node指令,直接執行某個文件:

let childProcess = execFile("node",['./test/sub_process'],function(err,stdout,stdin){
    console.log(stdout); 
});

內部調用的是 spawn方法。

4.4. exec

let childProcess = exec("node './test/sub_process'",function(err,stdout,stdin){
    console.log(stdout)
});

內部調用的是execFile,其實以上三個方法都是基於spawn的。

5. cluster

Node.js的單個實例在單個線程中運行。為了利用多核系統,用戶有時會希望啟動Node.js進程集群來處理負載。自己通過進程來實現集群。

子進程與父進程共享HTTP伺服器 fork實現:

let http = require('http');
let {
    fork
} = require('child_process');
let fs = require('fs');
let net = require('net');
let path = require('path');
let child = fork(path.join(__dirname, '8.child.js'));
let server = net.createServer();
server.listen(8080, '127.0.0.1', function () {
    child.send('server', server);
    console.log('父進程中的伺服器已經創建');
    let httpServer = http.createServer();
    httpServer.on('request', function (req, res) {
        if (req.url != '/favicon.ico') {
            let sum = 0;
            for (let i = 0; i < 100000; i++) {
                sum += 1;
            }
            res.write('客戶端請求在父進程中被處理。');
            res.end('sum=' + sum);
        }
    });
    httpServer.listen(server);
});

let http = require('http');
process.on('message', function (msg, server) {
    if (msg == 'server') {
        console.log('子進程中的伺服器已經被創建');
        let httpServer = http.createServer();
        httpServer.on('request', function (req, res) {
            if (req.url != '/favicon.ico') {
                sum = 0;
                for (let i = 0; i < 10000; i++) {
                    sum += i;
                }
                res.write('客戶端請求在子進程中被處理');
                res.end('sum=' + sum);
            }
        });
        httpServer.listen(server);
    }
});

進程與父進程共享socket對象:

let {
    fork
} = require('child_process');
let path = require('path');
let child = fork(path.join(__dirname, '11.socket.js'));
let server = require('net').createServer();
server.on('connection', function (socket) {
    if (Date.now() % 2 == 0) {
        child.send('socket', socket);
    } else {
        socket.end('客戶端請求被父進程處理!');
    }
});
server.listen(41234, );

process.on('message', function (m, socket) {
    if (m === 'socket') {
        socket.end('客戶端請求被子進程處理.');
    }
});

使用cluster模塊更加方便:

let cluster = require("cluster");
let http = require("http");
let cpus = require("os").cpus().length;
const workers = {};
if (cluster.isMaster) {
    cluster.on('exit',function(worker){
        console.log(worker.process.pid,'death')
        let w = cluster.fork();
        workers[w.pid] = w;
    })
  for (let i = 0; i < cpus; i++) {
    let worker = cluster.fork();
    workers[worker.pid] = worker;
  }
} else {
  http
    .createServer((req, res) => {
      res.end(process.pid+'','pid');
    })
    .listen(3000);
  console.log("server start",process.pid);
}

相關焦點

  • Node.js 診斷指南 第一彈
    實際上 Node.js 也內置了類似的機制,不過目前普遍認為 Node.js 內置的環境變量主要是用來排查 Node.js 內置的代碼和模塊用的。7 內部異常處理器運行時失敗:存在未捕獲的異常,並且內部致命異常處理函數本身在嘗試處理時拋出錯誤。例如,如果 'uncaughtException' 或 domain.on('error') 處理器拋出錯誤,就會發生這種情況。// 省略..
  • node.js、MongoDB下一代的LAMP
    node.js、MongoDB下一代的LAMP 我們大部分人在做網站時,都用的是LAMP,殊不知LAMP已成過去式,新一代的小生:nix、node.js、MongoDB誕生了,讓我們走進他們,知道他們的故事!
  • centos7編程實踐:安裝nodejs
    2、node.js的優勢2.1、Nodejs語法完全是js語法,只要你懂js基礎就可以學會Nodejs後端開發Node打破了過去JavaScript只能在瀏覽器中運行的局面。2.2、NodeJs超強的高並發能力NodeJs的首要目標是提供一種簡單的、用於創建高性能伺服器及可在該伺服器中運行的各種應用程式的開發工具。首先讓我們來看一下現在的伺服器端語言中存在著什麼問題。在Java、PHP或者.NET等伺服器語言中,會為每一個客戶端連接創建一個新的線程。
  • 方便快捷的調試 Node.js 程序
    Node.js 的異步工作流為這一艱巨的過程增加了額外的複雜性。儘管 V8 引擎為了方便訪問異步棧跟蹤進行了一些更新,但是在很多數情況下,我們只會在程序主線程上遇到錯誤,這使得調試有些困難。同樣,當我們的 Node.js 程序崩潰時,通常需要依靠一些複雜的 CLI 工具來分析核心轉儲[1]。
  • 15 個常見的 Node.js 面試問題及答案
    Node.js 是異步的、事件驅動的、非阻塞的和單線程的,使得它成為開發下面應用程式的完美候選:然而,Node.js 的特性使得它對於其他類型的應用程式來說不是一個理想的選擇。執行 CPU 密集型任務的應用程式(如複雜的數學計算)在使用 CPU 時表現不佳,因為 Node.js 是單線程的。
  • Node.js 學習資料和教程(值得收藏)
    >被誤解的 Node.jsNode.js C++ addon編寫實戰系列熱門node.js模塊排行榜,方便找出你想要的模塊nodejs多線程,真正的非阻塞淺析nodejs的buffer類利用libuv編寫異步多線程的addon實例Node.js中exports與module.exports的區別Node.js http 管道拒絕服務漏洞Node.js高級編程
  • Node.js 中實踐基於 Redis 的分布式鎖實現
    在一些分布式環境下、多線程並發編程中,如果對同一資源進行讀寫操作,避免不了的一個就是資源競爭問題,通過引入分布式鎖這一概念,可以解決數據一致性問題
  • 讓你 nodejs 水平暴增的 debugger 技巧
    debugger 的含義就是要在某個地方斷住,可以單步運行、查看環境中的變量。那麼怎麼設置斷點、怎麼把當前上下文的變量暴露出去呢,就是通過啟動一個 websocket server,這時候只要啟動一個 websocket client 連接上這個 server 就可以調試 nodejs 代碼了。
  • Nodejs 整體架構與九個核心模塊實現
    Nodejs 組成 Node.js 主要由 V8、Libuv 和第三方庫組成:Libuv:跨平臺的異步 IO 庫,但它提供的功能不僅僅是 IO,還包括進程、線程、信號、定時器、進程間通信,線程池等。線程和線程間通信 6.1 線程架構Node.js 是單線程的,為了方便用戶處理耗時的操作,Node.js 在支持多進程之後,又支持了多線程。Node.js 中多線程的架構如下圖所示,每個子線程本質上是一個獨立的事件循環,但是所有的線程會共享底層的 Libuv 線程池。
  • Node.js學習筆記第一天
    01-導入node模塊使用流程// node.js中將不同功能的代碼放在不同的js文件中,也叫模塊化,核心模塊會隨著安裝node.js時一併安裝// 1. fs.readFile('./123.txt', 'utf-8', (err, data) => {  // 沒有錯誤信息 err 為 null  // 如果有錯誤信息 if成立 執行 throw 程序終止並拋出異常  if (err) throw err;    // 代碼能執行到這裡說明沒有錯誤信息  // 就可以使用data參數來拿到從文件中讀取到的數據
  • 一篇文章構建你的 Node.js 知識體系
    CPU 問題通過 master-cluster 模式可以使得應用更加健壯Cluster 底層是 child_process 模塊,除了可以發送普通消息,還可以發送底層對象 TCP、UDP 等TCP 主進程發送到子進程,子進程能根據消息重建出 TCP 連接,Cluster 可以決定 fork 出合適的硬體資源的子進程數Node 多線程單線程問題Node 線程Node 中最核心的是 v8 引擎,
  • 一篇文章構建你的 NodeJS 知識體系
    CPU 問題通過 master-cluster 模式可以使得應用更加健壯Cluster 底層是 child_process 模塊,除了可以發送普通消息,還可以發送底層對象 TCP、UDP 等TCP 主進程發送到子進程,子進程能根據消息重建出 TCP 連接,Cluster 可以決定 fork 出合適的硬體資源的子進程數Node 多線程單線程問題Node 線程Node 中最核心的是 v8 引擎,
  • Node.js 入門你需要知道的 10 個問題
    Node.js 功能以下是 Node.js 的一些重要功能Node.js 使用的單線程模型且採用了事件循環架構,使得編寫可擴展性高的伺服器變得既容易又安全。一些傳統的服務端語言會創建多線程來處理請求,通常創建線程都是有系統資源開銷的,因此也會有一些限制,而 Node.js 只創建一個線程來處理更多的請求。
  • Node.js是用來做什麼的?
    在下面的 「Hello World」 示例中,可以並發處理許多連接,每一個連接都會觸發一個回調,而當沒有可做的事情時,Node.js 就會進入休眠狀態。這與當今比較常見的採用作業系統線程的並發模型形成了鮮明對比。基於線程的網絡效率相對較低且更難以使用。此外,由於沒有鎖,Node.js 的用戶不用擔心進程死鎖的問題。
  • 專門針對初學者的Node.js教程
    異步回調正如在上例中看到的那樣,Node.js典型的模式是使用異步回調。基本上,你告訴Node.js要做的事,它執行完後便會調用你的函數(回調函數)。這是因為Node是單線程的。在你等待回調函數執行過程中,Node可繼續執行其他事務,不必被阻塞直到該請求完畢。這對於Web伺服器尤其重要。
  • 在 Node.js 中引入 Golang ,會讓它更快嗎?
    同時,我們有很多方式來處理此類任務(子進程/集群、工作線程)。此外,還有可能使用其他語言(C、C++、Rust、Golang)作為單獨的服務/微服務或通過 WebAssembly 腳本進行調用。之前我也寫過一篇,在 React 項目中引入 Rust 的文章,感興趣可以看:使用 Rust 編寫更快的 React 組件最近發現了一個老外做了在 Node.js 服務中引入 Golang 的性能測試(https://blog.devgenius.io/node-js-in-go-we-trust
  • 一篇文章構建你的 NodeJS 知識體系(W字長文)
    CPU 問題通過 master-cluster 模式可以使得應用更加健壯Cluster 底層是 child_process 模塊,除了可以發送普通消息,還可以發送底層對象 TCP、UDP 等TCP 主進程發送到子進程,子進程能根據消息重建出 TCP 連接,Cluster 可以決定 fork 出合適的硬體資源的子進程數Node 多線程單線程問題Node 線程Node 中最核心的是 v8 引擎,
  • 滲透測試中的Node.js——Downloader的實現
    我最近在一篇文章中學到了利用Node.js繞過主動防禦的技巧,於是對Node.js的語法進行了學習,開源一個Downloader的實現代碼,分享腳本開發中需要注意的細節。Node.js繞過主動防禦的學習地址:https://bbs.pediy.com/thread-249573.htm
  • 寫 Node.js 代碼,從學會調試開始
    另一方面,你可以控制代碼執行的邏輯,你可以暫定執行,或者逐行運行,甚至修改內存中的值,讓它走到另一個分支裡。Node.js 內置的調試使用 Node.js 內置的調試方式是最簡單直接的,但是現階段都有 IDE,所以大家都不太關心底層的實現,一鍵開啟調試就行了。
  • Node.js 有難度的面試題,你能答對幾個?
    使用 heapdump 保存內存快照時,只會有 Node.js 環境中的對象,不會受到幹擾(如果使用 node-inspector 的話,快照中會有前端的變量幹擾)。PS:安裝 heapdump 在某些 Node.js 版本上可能出錯,建議使用 npm install heapdump -target=Node.js 版本來安裝。