Node.js 中的進程和線程

来源:https://www.cnblogs.com/dtux/archive/2022/05/07/16242359.html
-Advertisement-
Play Games

線程和進程是電腦操作系統的基礎概念,在程式員中屬於高頻辭彙,那如何理解呢?Node.js 中的進程和線程又是怎樣的呢? 一、進程和線程 1.1、專業性文字定義 進程(Process),進程是電腦中的程式關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎,進程 ...


線程和進程是電腦操作系統的基礎概念,在程式員中屬於高頻辭彙,那如何理解呢?Node.js 中的進程和線程又是怎樣的呢?

file

一、進程和線程

1.1、專業性文字定義

  • 進程(Process),進程是電腦中的程式關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操作系統結構的基礎,進程是線程的容器。
  • 線程(Thread),線程是操作系統能夠進行運算調度的最小單位,被包含在進程之中,是進程中的實際運作單位。

1.2、通俗理解

以上描述比較硬,看完可能也沒看懂,還不利於理解記憶。那麼我們舉個簡單的例子:

假設你是某個快遞站點的一名小哥,起初這個站點負責的區域住戶不多,收取件都是你一個人。給張三家送完件,再去李四家取件,事情得一件件做,這叫單線程,所有的工作都得按順序執行
後來這個區域住戶多了,站點給這個區域分配了多個小哥,還有個小組長,你們可以為更多的住戶服務了,這叫多線程,小組長是主線程,每個小哥都是一個線程
快遞站點使用的小推車等工具,是站點提供的,小哥們都可以使用,並不僅供某一個人,這叫多線程資源共用。
站點小推車目前只有一個,大家都需要使用,這叫衝突。解決的方法有很多,排隊等待或者等其他小哥用完後的通知,這叫線程同步

file

總公司有很多站點,各個站點的運營模式幾乎一模一樣,這叫多進程。總公司叫主進程,各個站點叫子進程
總公司和站點之間,以及各個站點互相之間,小推車都是相互獨立的,不能混用,這叫進程間不共用資源。各站點間可以通過電話等方式聯繫,這叫管道。各站點間還有其他協同手段,便於完成更大的計算任務,這叫進程間同步

還可以看看阮一峰的 進程與線程的一個簡單解釋

二、Node.js 中的進程和線程

Node.js 是單線程服務,事件驅動和非阻塞 I/O 模型的語言特性,使得 Node.js 高效和輕量。優勢在於免去了頻繁切換線程和資源衝突;擅長 I/O 密集型操作(底層模塊 libuv 通過多線程調用操作系統提供的非同步 I/O 能力進行多任務的執行),但是對於服務端的 Node.js,可能每秒有上百個請求需要處理,當面對 CPU 密集型請求時,因為是單線程模式,難免會造成阻塞。

2.1、Node.js 阻塞

我們利用 Koa 簡單地搭建一個 Web 服務,用斐波那契數列方法來模擬一下 Node.js 處理 CPU 密集型的計算任務:

斐波那契數列,也稱黃金分割數列,這個數列從第三項開始,每一項都等於前兩項只和:0、1、1、2、3、5、8、13、21、......

// app.js
const Koa = require('koa')
const router = require('koa-router')()
const app = new Koa()

// 用來測試是否被阻塞
router.get('/test', (ctx) => {
    ctx.body = {
        pid: process.pid,
        msg: 'Hello World'
    }
})
router.get('/fibo', (ctx) => {
    const { num = 38 } = ctx.query
    const start = Date.now()
    // 斐波那契數列
    const fibo = (n) => {
        return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1
    }
    fibo(num)

    ctx.body = {
        pid: process.pid,
        duration: Date.now() - start
    }
})

app.use(router.routes())
app.listen(9000, () => {
    console.log('Server is running on 9000')
})

執行 node app.js 啟動服務,用 Postman 發送請求,可以看到,計算 38 次耗費了 617ms,換而言之,因為執行了一個 CPU 密集型的計算任務,所以 Node.js 主線程被阻塞了六百多毫秒。如果同時處理更多的請求,或者計算任務更複雜,那麼在這些請求之後的所有請求都會被延遲執行。

file

我們再新建一個 axios.js 用來模擬發送多次請求,此時將 app.js 中的 fibo 計算次數改為 43,用來模擬更複雜的計算任務:

// axios.js
const axios = require('axios')

const start = Date.now()
const fn = (url) => {
    axios.get(`http://127.0.0.1:9000/${ url }`).then((res) => {
        console.log(res.data, `耗時: ${ Date.now() - start }ms`)
    })
}

fn('test')
fn('fibo?num=43')
fn('test')

file

可以看到,當請求需要執行 CPU 密集型的計算任務時,後續的請求都被阻塞等待,這類請求一多,服務基本就阻塞卡死了。對於這種不足,Node.js 一直在彌補。

2.2、master-worker

master-worker 模式是一種並行模式,核心思想是:系統有兩個及以上的進程或線程協同工作時,master 負責接收和分配並整合任務,worker 負責處理任務。

file

2.3、多線程

線程是 CPU 調度的一個基本單位,只能同時執行一個線程的任務,同一個線程也只能被一個 CPU 調用。如果使用的是多核 CPU,那麼將無法充分利用 CPU 的性能。

多線程帶給我們靈活的編程方式,但是需要學習更多的 Api 知識,在編寫更多代碼的同時也存在著更多的風險,線程的切換和鎖也會增加系統資源的開銷。

worker_threads 是 Node.js 提供的一種多線程 Api。對於執行 CPU 密集型的計算任務很有用,對 I/O 密集型的操作幫助不大,因為 Node.js 內置的非同步 I/O 操作比 worker_threads 更高效。worker_threads 中的 Worker,parentPort 主要用於子線程和主線程的消息交互。

將 app.js 稍微改動下,將 CPU 密集型的計算任務交給子線程計算:

// app.js
const Koa = require('koa')
const router = require('koa-router')()
const { Worker } = require('worker_threads')
const app = new Koa()

// 用來測試是否被阻塞
router.get('/test', (ctx) => {
    ctx.body = {
        pid: process.pid,
        msg: 'Hello World'
    }
})
router.get('/fibo', async (ctx) => {
    const { num = 38 } = ctx.query
    ctx.body = await asyncFibo(num)
})

const asyncFibo = (num) => {
    return new Promise((resolve, reject) => {
        // 創建 worker 線程並傳遞數據
        const worker = new Worker('./fibo.js', { workerData: { num } })
        // 主線程監聽子線程發送的消息
        worker.on('message', resolve)
        worker.on('error', reject)
        worker.on('exit', (code) => {
            if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`))
        })
    })
}

app.use(router.routes())
app.listen(9000, () => {
    console.log('Server is running on 9000')
})

新增 fibo.js 文件,用來處理複雜計算任務:

const { workerData, parentPort } = require('worker_threads')
const { num } = workerData

const start = Date.now()
// 斐波那契數列
const fibo = (n) => {
    return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1
}
fibo(num)

parentPort.postMessage({
    pid: process.pid,
    duration: Date.now() - start
})

執行上文的 axios.js,此時將 app.js 中的 fibo 計算次數改為 43,用來模擬更複雜的計算任務:

file

可以看到,將 CPU 密集型的計算任務交給子線程處理時,主線程不再被阻塞,只需等待子線程處理完成後,主線程接收子線程返回的結果即可,其他請求不再受影響。
上述代碼是演示創建 worker 線程的過程和效果,實際開發中,請使用線程池來代替上述操作,因為頻繁創建線程也會有資源的開銷。

線程是 CPU 調度的一個基本單位,只能同時執行一個線程的任務,同一個線程也只能被一個 CPU 調用。

我們再回味下,本小節開頭提到的線程和 CPU 的描述,此時由於是新的線程,可以在其他 CPU 核心上執行,可以更充分的利用多核 CPU。

2.4、多進程

Node.js 為了能充分利用 CPU 的多核能力,提供了 cluster 模塊,cluster 可以通過一個父進程管理多個子進程的方式來實現集群的功能。

  • child_process 子進程,衍生新的 Node.js 進程並使用建立的 IPC 通信通道調用指定的模塊。
  • cluster 集群,可以創建共用伺服器埠的子進程,工作進程使用 child_process 的 fork 方法衍生。

cluster 底層就是 child_process,master 進程做總控,啟動 1 個 agent 進程和 n 個 worker 進程,agent 進程處理一些公共事務,比如日誌等;worker 進程使用建立的 IPC(Inter-Process Communication)通信通道和 master 進程通信,和 master 進程共用服務埠。

file

新增 fibo-10.js,模擬發送 10 次請求:

// fibo-10.js
const axios = require('axios')

const url = `http://127.0.0.1:9000/fibo?num=38`
const start = Date.now()

for (let i = 0; i < 10; i++) {
    axios.get(url).then((res) => {
        console.log(res.data, `耗時: ${ Date.now() - start }ms`)
    })
}

可以看到,只使用了一個進程,10 個請求慢慢阻塞,累計耗時 15 秒:

file

接下來,將 app.js 稍微改動下,引入 cluster 模塊:

// app.js
const cluster = require('cluster')
const http = require('http')
const numCPUs = require('os').cpus().length
// const numCPUs = 10 // worker 進程的數量一般和 CPU 核心數相同
const Koa = require('koa')
const router = require('koa-router')()
const app = new Koa()

// 用來測試是否被阻塞
router.get('/test', (ctx) => {
    ctx.body = {
        pid: process.pid,
        msg: 'Hello World'
    }
})
router.get('/fibo', (ctx) => {
    const { num = 38 } = ctx.query
    const start = Date.now()
    // 斐波那契數列
    const fibo = (n) => {
        return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1
    }
    fibo(num)

    ctx.body = {
        pid: process.pid,
        duration: Date.now() - start
    }
})
app.use(router.routes())

if (cluster.isMaster) {
    console.log(`Master ${process.pid} is running`)
    
    // 衍生 worker 進程
    for (let i = 0; i < numCPUs; i++) {
        cluster.fork()
    }

    cluster.on('exit', (worker, code, signal) => {
        console.log(`worker ${worker.process.pid} died`)
    })
} else {
    app.listen(9000)
    console.log(`Worker ${process.pid} started`)
}

執行 node app.js 啟動服務,可以看到,cluster 幫我們創建了 1 個 master 進程和 4 個 worker 進程:

file

file

通過 fibo-10.js 模擬發送 10 次請求,可以看到,四個進程處理 10 個請求耗時近 9 秒:

file

當啟動 10 個 worker 進程時,看看效果:

file

僅需不到 3 秒,不過進程的數量也不是無限的。在日常開發中,worker 進程的數量一般和 CPU 核心數相同。

2.5、多進程說明

開啟多進程不全是為了處理高併發,而是為瞭解決 Node.js 對於多核 CPU 利用率不足的問題。
由父進程通過 fork 方法衍生出來的子進程擁有和父進程一樣的資源,但是各自獨立,互相之間資源不共用。通常根據 CPU 核心數來設置進程數量,因為系統資源是有限的。

三、總結

1、大部分通過多線程解決 CPU 密集型計算任務的方案都可以通過多進程方案來替代;
2、Node.js 雖然非同步,但是不代表不會阻塞,CPU 密集型任務最好不要在主線程處理,保證主線程的暢通;
3、不要一味的追求高性能和高併發,達到系統需要即可,高效、敏捷才是項目需要的,這也是 Node.js 輕量的特點。
4、Node.js 中的進程和線程還有很多概念在文章中提到了但沒展開細講或沒提到的,比如:Node.js 底層 I/O 的 libuv、IPC 通信通道、多進程如何守護、進程間資源不共用如何處理定時任務、agent 進程等;
5、以上代碼可在 https://github.com/liuxy0551/node-process-thread 查看。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 解決MySQL 8.0在Linux環境下的安裝、初始化、配置。參考環境:MySQL Community Server 8.0.28;CentOS Linux release 7.9.2009。 ...
  • 本文收集了各種資料庫的SQL語句優化原理思路、技術要點和方法實操案例文檔!希望大家都能寫得一手好SQL、掌握資料庫高性能運行秘訣! ...
  • HarmonyOS Connect智能硬體開放生態即將步入富設備產業化時代!為了讓廣大開發者能搶先體驗鴻蒙智聯富設備開發,本期我們將為大家帶來七款支持富設備開發的開發板。 ...
  • 精準推送是移動端產品留存階段的主要運營手段,精準推送常常會與用戶畫像緊密結合,針對用戶的喜好、畫像,採用不同策略,但基於用戶所屬區域推送消息卻很難實現。目前市面上大多數第三方消息推送服務商,在系統未深度定製的情況下,通常不支持將推送人群範圍精確到某個商圈或較小的區域,而地理圍欄技術可以很好地彌補這一 ...
  • 前言 本文主要是整理了使用WebRTC做音視頻通訊時的各知識點及問題點。有理解不足和不到位的地方也歡迎指正。 對於你感興趣的部分可以選擇性觀看。 WebRTC的初始化 在使用WebRTC的庫之前,需要對WebRTC進行初始化, 用到的代碼如下: RTCInitializeSSL(); 轉定義後可以看 ...
  • 今天的內容有意思了,朋友們繼續對我們之前的案例完善,是這樣的我們之前是不是靠props來完成父給子,子給父之間傳數據,其實父給子最好的方法就是props但是自給父就不是了,並且今天學下來,不僅如此,組件間任何層級的關係我都可以傳數據了,兄弟之間,爺孫之間等等等等 七.瀏覽器本地存儲 1.localS ...
  • 移動端瀑布流佈局是一種比較流行的網頁佈局方式,視覺上來看就是一種像瀑布一樣垂直落下的排版。每張圖片並不是顯示的正正方方的,而是有的長有的短,呈現出一種不規則的形狀。但是它們的寬度通常都是相同的 因為移動端瀑布流佈局主要為豎向瀑布流,因此本文所探討的是豎向瀑布流 特點 豎向瀑布流佈局主要有下麵幾種特點 ...
  • 可以將( 0, null, false, undefined, NaN )理解為數字 0 與運算: 與運算 類比四則運算中的乘法。0和任何數相乘都等於0,因此他們和其他值做與運算都等於0(等於他本身,例如:null && 'abc',結果為 null;1414 && 0,結果為 0)。 若是兩個0 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...