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
  • 分組和樹形結構是不一樣的。 樹形結構是以遞歸形式存在。分組是以鍵值對存在的形式,類似於GroupBy這樣的形式。 舉個例子 ID NAME SEX Class 1 張三 男 1 2 李四 女 2 3 王二 男 1 當以Sex為分組依據時則是 Key Value 男 1 張三 男 1 3 王二 男 1 ...
  • NetCore中將SQLServer資料庫備份為Sql腳本 描述: 最近寫項目收到了一個需求, 就是將SQL Server資料庫備份為Sql腳本, 如果是My Sql之類的還好說, 但是在網上搜了一大堆, 全是教你怎麼操作SSMS的, 就很d疼! 解決方案: 通過各種查找資料, 還有一些老哥的幫助, ...
  • 我的Notion Clowd.Squirrel Squirrel.Windows 是一組工具和適用於.Net的庫,用於管理 Desktop Windows 應用程式的安裝和更新。 Squirrel.Windows 對 Windows 應用程式的實現語言沒有任何要求,甚至無需服務端即可完成增量更新。 ...
  • 轉載請註明來源 https://www.cnblogs.com/brucejiao/p/16188865.html 謝謝! 轉載請註明來源 https://www.cnblogs.com/brucejiao/p/16188865.html 謝謝! 轉載請註明來源 https://www.cnblog ...
  • 1. Netty源碼研究筆記(3)——Channel系列 依舊是通過先縱向再橫向的研究方法,在開篇中,我們發現不管是Sever還是Client,最終的啟動是通過調用channel的對應方法來完成的,而這個動作實際在channel綁定的eventLoop中執行。 接下來,我們繼續EchoSever、E ...
  • 大家好,今天給大家介紹一款輕量、快速、穩定可編排的組件式規則引擎框架LiteFlow。 一、LiteFlow的介紹 LiteFlow官方網站和代碼倉庫地址 官方網站:https://yomahub.com/liteflow Gitee托管倉庫:https://gitee.com/dromara/li ...
  • 我使用Spring AOP實現了用戶操作日誌功能 今天答辯完了,復盤了一下系統,發現還是有一些東西值得拿出來和大家分享一下。 需求分析 系統需要對用戶的操作進行記錄,方便未來溯源 首先想到的就是在每個方法中,去實現記錄的邏輯,但是這樣做肯定是不現實的,首先工作量大,其次違背了軟體工程設計原則(開閉原 ...
  • 《零基礎學Java》 繪製幾何圖形 Java可以分別使用 Graphics 和 Graphics2D 繪製圖形,Graphics類 使用不同的方法繪製不同的圖形(drawLine()方法可f以繪製線、drawRect()方法用於繪製矩形、drawOval()方法用於繪製橢圓形)。 Graphics類 ...
  • 本期教程人臉識別第三方平臺為虹軟科技,本文章講解的是人臉識別RGB活體追蹤技術,免費的功能很多可以自行搭配,希望在你看完本章課程有所收穫。 ...
  • 很多人都喜歡使用黑色的主題樣式,包括我自己,使用了差不多三年的黑色主題,但是個人覺得在進行視窗轉換的時候很廢眼睛。 比如IDEA是全黑的,然後需要看PDF或者WORD又變成白色的了,這樣來回切換導致眼睛很累,畢竟現在網頁以及大部分軟體的界面都是白色的。那麼還是老老實實的使用原來比較順眼的模式吧。 1 ...