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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...