您好,登錄后才能下訂單哦!
這篇文章主要介紹“NodeJS中的進程管理怎么實現(xiàn)”,在日常操作中,相信很多人在NodeJS中的進程管理怎么實現(xiàn)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”NodeJS中的進程管理怎么實現(xiàn)”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
熟悉 js 的朋友都知道,js 是單線程
的,在 Node 中,采用的是 多進程單線程 的模型。由于javascript單線程的限制,在多核服務器上,我們往往需要啟動多個進程才能最大化服務器性能。
Node.js 進程集群可用于運行多個 Node.js 實例,這些實例可以在其應用程序線程之間分配工作負載。 當不需要進程隔離時,請改用 worker_threads
模塊,它允許在單個 Node.js 實例中運行多個應用程序線程。
進程總數(shù),其中一個主進程,cpu 個數(shù) x cpu 核數(shù) 個 子進程
無論 child_process 還是 cluster,都不是多線程模型,而是多進程模型
應對單線程問題,通常使用多進程的方式來模擬多線程
Node 在 V0.8 版本之后引入了 cluster模塊,通過一個主進程 (master) 管理多個子進程 (worker) 的方式實現(xiàn)集群
。
集群模塊可以輕松創(chuàng)建共享服務器端口的子進程。
cluster 底層是 child_process 模塊,除了可以發(fā)送普通消息,還可以發(fā)送底層對象
TCP
、UDP
等,cluster
模塊是child_process
模塊和net
模塊的組合應用。 cluster 啟動時,內(nèi)部會啟動 TCP 服務器,將這個 TCP 服務器端 socket 的文件描述符發(fā)給工作進程。
在 cluster
模塊應用中,一個主進程只能管理一組工作進程
,其運作模式?jīng)]有 child_process
模塊那么靈活,但是更加穩(wěn)定:
const cluster = require('cluster')復
.isMaster
標識主進程, Node<16
.isPrimary
標識主進程, Node>16
.isWorker
標識子進程
.worker
對當前工作進程對象的引用【子進程中】
.workers
存儲活動工作進程對象的哈希,以 id
字段為鍵。 這樣可以很容易地遍歷所有工作進程。 它僅在主進程中可用。cluster.wokers[id] === worker
【主進程中】
.settings
只讀, cluster配置項。在調(diào)用 .setupPrimary()或.fork()方法之后,此設置對象將包含設置,包括默認值。之前為空對象。此對象不應手動更改或設置。
cluster.settings
配置項詳情:- `execArgv` <string[]>傳給 Node.js 可執(zhí)行文件的字符串參數(shù)列表。 **默認值:** `process.execArgv`。 - `exec` <string> 工作進程文件的文件路徑。 **默認值:** `process.argv[1]`。 - `args` <string[]> 傳給工作進程的字符串參數(shù)。 **默認值:**`process.argv.slice(2)`。 - `cwd` <string>工作進程的當前工作目錄。 **默認值:** `undefined` (從父進程繼承)。 - `serialization` <string>指定用于在進程之間發(fā)送消息的序列化類型。 可能的值為 `'json'` 和 `'advanced'`。 **默認值:** `false`。 - `silent` <boolean>是否將輸出發(fā)送到父進程的標準輸入輸出。 **默認值:** `false`。 - `stdio` <Array>配置衍生進程的標準輸入輸出。 由于集群模塊依賴 IPC 來運行,因此此配置必須包含 `'ipc'` 條目。 提供此選項時,它會覆蓋 `silent`。 - `uid` <number>設置進程的用戶標識。 - `gid` <number>設置進程的群組標識。 - `inspectPort` <number> | <Function> 設置工作進程的檢查器端口。 這可以是數(shù)字,也可以是不帶參數(shù)并返回數(shù)字的函數(shù)。 默認情況下,每個工作進程都有自己的端口,從主進程的 `process.debugPort` 開始遞增。 - `windowsHide` <boolean> 隱藏通常在 Windows 系統(tǒng)上創(chuàng)建的衍生進程控制臺窗口。 **默認值:** `false`。
.fork([env])
衍生新的工作進程【主進程中】
.setupPrimary([settings])
Node>16
.setupMaster([settings])
用于更改默認的 'fork' 行為,用后設置將出現(xiàn)在 cluster.settings
中。任何設置更改只會影響未來對 .fork()
的調(diào)用,而不會影響已經(jīng)運行的工作進程。上述默認值僅適用于第一次調(diào)用。Node 小于 16【主進程中】
.disconnect([callback])
當所有工作進程斷開連接并關閉句柄時調(diào)用【主進程中】
為了讓集群更加穩(wěn)定和健壯,cluster
模塊也暴露了許多事件:
'message'
事件, 當集群主進程接收到來自任何工作進程的消息時觸發(fā)。
'exit'
事件, 當任何工作進程死亡時,則集群模塊將觸發(fā) 'exit'
事件。
cluster.on('exit', (worker, code, signal) => { console.log('worker %d died (%s). restarting...', worker.process.pid, signal || code); cluster.fork(); });
'listening'
事件,從工作進程調(diào)用 listen()
后,當服務器上觸發(fā) 'listening'
事件時,則主進程中的 cluster
也將觸發(fā) 'listening'
事件。
cluster.on('listening', (worker, address) => { console.log( `A worker is now connected to ${address.address}:${address.port}`); });
'fork'
事件,當新的工作進程被衍生時,則集群模塊將觸發(fā) 'fork'
事件。
cluster.on('fork', (worker) => { timeouts[worker.id] = setTimeout(errorMsg, 2000); });
'setup'
事件,每次調(diào)用 .setupPrimary()
時觸發(fā)。
disconnect
事件,在工作進程 IPC 通道斷開連接后觸發(fā)。 當工作進程正常退出、被殺死、或手動斷開連接時
cluster.on('disconnect', (worker) => { console.log(`The worker #${worker.id} has disconnected`); });
Worker
對象包含了工作進程的所有公共的信息和方法。 在主進程中,可以使用 cluster.workers
來獲取它。 在工作進程中,可以使用 cluster.worker
來獲取它。
.id
工作進程標識,每個新的工作進程都被賦予了自己唯一的 id,此 id 存儲在 id
。當工作進程存活時,這是在 cluster.workers
中索引它的鍵。
.process
所有工作進程都是使用 child_process.fork()
創(chuàng)建,此函數(shù)返回的對象存儲為 .process
。 在工作進程中,存儲了全局的 process
。
.send(message[, sendHandle[, options]][, callback])
向工作進程或主進程發(fā)送消息,可選擇使用句柄。在主進程中,這會向特定的工作進程發(fā)送消息。 它與 ChildProcess.send()
相同。在工作進程中,這會向主進程發(fā)送消息。 它與 process.send()
相同。
.destroy()
.kill([signal])
此函數(shù)會殺死工作進程。kill()
函數(shù)在不等待正常斷開連接的情況下殺死工作進程,它與 worker.process.kill()
具有相同的行為。為了向后兼容,此方法別名為 worker.destroy()
。
.disconnect([callback])
發(fā)送給工作進程,使其調(diào)用自身的 .disconnect()
將關閉所有服務器,等待那些服務器上的 'close'
事件,然后斷開 IPC 通道。
.isConnect()
如果工作進程通過其 IPC 通道連接到其主進程,則此函數(shù)返回 true
,否則返回 false
。 工作進程在創(chuàng)建后連接到其主進程。
.isDead()
如果工作進程已終止(由于退出或收到信號),則此函數(shù)返回 true
。 否則,它返回 false
。
為了讓集群更加穩(wěn)定和健壯,cluster
模塊也暴露了許多事件:
'message'
事件, 在工作進程中。
cluster.workers[id].on('message', messageHandler);
'exit'
事件, 當任何工作進程死亡時,則當前worker工作進程
對象將觸發(fā) 'exit'
事件。
if (cluster.isPrimary) { const worker = cluster.fork(); worker.on('exit', (code, signal) => { if (signal) { console.log(`worker was killed by signal: ${signal}`); } else if (code !== 0) { console.log(`worker exited with error code: ${code}`); } else { console.log('worker success!'); } }); }
'listening'
事件,從工作進程調(diào)用 listen()
,對當前工作進程進行監(jiān)聽。
cluster.fork().on('listening', (address) => { // 工作進程正在監(jiān)聽 });
disconnect
事件,在工作進程 IPC 通道斷開連接后觸發(fā)。 當工作進程正常退出、被殺死、或手動斷開連接時
cluster.fork().on('disconnect', () => { //限定于當前worker對象觸發(fā) });
Node中主進程和子進程之間通過進程間通信 (IPC) 實現(xiàn)進程間的通信,進程間通過 .send()
(a.send表示向a發(fā)送)方法發(fā)送消息,監(jiān)聽 message
事件收取信息,這是 cluster模塊
通過集成 EventEmitter
實現(xiàn)的。還是一個簡單的官網(wǎng)的進程間通信例子
子進程:process.on('message')
、process.send()
父進程:child.on('message')
、child.send()
# cluster.isMaster # cluster.fork() # cluster.workers # cluster.workers[id].on('message', messageHandler); # cluster.workers[id].send(); # process.on('message', messageHandler); # process.send(); const cluster = require('cluster'); const http = require('http'); # 主進程 if (cluster.isMaster) { // Keep track of http requests console.log(`Primary ${process.pid} is running`); let numReqs = 0; // Count requests function messageHandler(msg) { if (msg.cmd && msg.cmd === 'notifyRequest') { numReqs += 1; } } // Start workers and listen for messages containing notifyRequest // 開啟多進程(cpu核心數(shù)) // 衍生工作進程。 const numCPUs = require('os').cpus().length; for (let i = 0; i < numCPUs; i++) { console.log(i) cluster.fork(); } // cluster worker 主進程與子進程通信 for (const id in cluster.workers) { // ***監(jiān)聽來自子進程的事件 cluster.workers[id].on('message', messageHandler); // ***向子進程發(fā)送 cluster.workers[id].send({ type: 'masterToWorker', from: 'master', data: { number: Math.floor(Math.random() * 50) } }); } cluster.on('exit', (worker, code, signal) => { console.log(`worker ${worker.process.pid} died`); }); } else { # 子進程 // 工作進程可以共享任何 TCP 連接 // 在本示例中,其是 HTTP 服務器 // Worker processes have a http server. http.Server((req, res) => { res.writeHead(200); res.end('hello world\n'); //****** ?。。。otify master about the request ?。。。。?!******* //****** 向process發(fā)送 process.send({ cmd: 'notifyRequest' }); //****** 監(jiān)聽從process來的 process.on('message', function(message) { // xxxxxxx }) }).listen(8000); console.log(`Worker ${process.pid} started`); }
NodeJS 進程之間通信只有消息傳遞,不會真正的傳遞對象。
send()
方法在發(fā)送消息前,會將消息組裝成 handle 和 message,這個 message 會經(jīng)過 JSON.stringify
序列化,也就是說,傳遞句柄的時候,不會將整個對象傳遞過去,在 IPC 通道傳輸?shù)亩际亲址?,傳輸后通過 JSON.parse
還原成對象。
代碼里有 app.listen(port)
在進行 fork 時,為什么多個進程可以監(jiān)聽同一個端口呢?
原因是主進程通過 send() 方法向多個子進程發(fā)送屬于該主進程的一個服務對象的句柄,所以對于每一個子進程而言,它們在還原句柄之后,得到的服務對象是一樣的,當網(wǎng)絡請求向服務端發(fā)起時,進程服務是搶占式的,所以監(jiān)聽相同端口時不會引起異常。
看下端口被占用的情況:
# master.js const fork = require('child_process').fork; const cpus = require('os').cpus(); for (let i=0; i<cpus.length; i++) { const worker = fork('worker.js'); console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); }
# worker.js const http = require('http'); http.createServer((req, res) => { res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); }).listen(3000);
以上代碼示例,控制臺執(zhí)行
node master.js
只有一個 worker 可以監(jiān)聽到 3000 端口,其余將會拋出Error: listen EADDRINUSE :::3000
錯誤。
那么多進程模式下怎么實現(xiàn)多進程端口監(jiān)聽呢?答案還是有的,通過句柄傳遞 Node.js v0.5.9 版本之后支持進程間可發(fā)送句柄
功能
/** * http://nodejs.cn/api/child_process.html#child_process_subprocess_send_message_sendhandle_options_callback * message * sendHandle */ subprocess.send(message, sendHandle)
當父子進程之間建立 IPC 通道之后,通過子進程對象的 send 方法發(fā)送消息,第二個參數(shù) sendHandle 就是句柄,可以是 TCP套接字、TCP服務器、UDP套接字等
,為了解決上面多進程端口占用問題,我們將主進程的 socket 傳遞到子進程。
# master.js const fork = require('child_process').fork; const cpus = require('os').cpus(); const server = require('net').createServer(); server.listen(3000); process.title = 'node-master' for (let i=0; i<cpus.length; i++) { const worker = fork('worker.js'); # 句柄傳遞 worker.send('server', server); console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); }
// worker.js let worker; process.title = 'node-worker' process.on('message', function (message, sendHandle) { if (message === 'server') { worker = sendHandle; worker.on('connection', function (socket) { console.log('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid) }); } });
驗證一番,控制臺執(zhí)行 node master.js
了解 cluster
的話會知道,子進程是通過 cluster.fork()
創(chuàng)建的。在 linux 中,系統(tǒng)原生提供了 fork
方法,那么為什么 Node 選擇自己實現(xiàn) cluster模塊
,而不是直接使用系統(tǒng)原生的方法?主要的原因是以下兩點:
fork的進程監(jiān)聽同一端口會導致端口占用錯誤
fork的進程之間沒有負載均衡,容易導致驚群現(xiàn)象
在 cluster模塊
中,針對第一個問題,通過判斷當前進程是否為 master進程
,若是,則監(jiān)聽端口,若不是則表示為 fork 的 worker進程
,不監(jiān)聽端口。
針對第二個問題,cluster模塊
內(nèi)置了負載均衡功能, master進程
負責監(jiān)聽端口接收請求,然后通過調(diào)度算法(默認為 Round-Robin,可以通過環(huán)境變量 NODE_CLUSTER_SCHED_POLICY
修改調(diào)度算法)分配給對應的 worker進程
。
當代碼拋出了異常沒有被捕獲到時,進程將會退出,此時 Node.js 提供了 process.on('uncaughtException', handler)
接口來捕獲它,但是當一個 Worker 進程遇到未捕獲的異常時,它已經(jīng)處于一個不確定狀態(tài),此時我們應該讓這個進程優(yōu)雅退出:
關閉異常 Worker 進程所有的 TCP Server(將已有的連接快速斷開,且不再接收新的連接),斷開和 Master 的 IPC 通道,不再接受新的用戶請求。
Master 立刻 fork 一個新的 Worker 進程,保證在線的『工人』總數(shù)不變。
異常 Worker 等待一段時間,處理完已經(jīng)接受的請求后退出。
+---------+ +---------+ | Worker | | Master | +---------+ +----+----+ | uncaughtException | +------------+ | | | | +---------+ | <----------+ | | Worker | | | +----+----+ | disconnect | fork a new worker | +-------------------------> + ---------------------> | | wait... | | | exit | | +-------------------------> | | | | | die | | | | | |
當一個進程出現(xiàn)異常導致 crash 或者 OOM 被系統(tǒng)殺死時,不像未捕獲異常發(fā)生時我們還有機會讓進程繼續(xù)執(zhí)行,只能夠讓當前進程直接退出,Master 立刻 fork 一個新的 Worker。
child_process 模塊提供了衍生子進程的能力, 簡單來說就是執(zhí)行cmd命令的能力
。
默認情況下, stdin、 stdout 和 stderr 的管道會在父 Node.js 進程和衍生的子進程之間建立
。 這些管道具有有限的(且平臺特定的)容量。 如果子進程寫入 stdout 時超出該限制且沒有捕獲輸出,則子進程會阻塞并等待管道緩沖區(qū)接受更多的數(shù)據(jù)。 這與 shell 中的管道的行為相同。 如果不消費輸出,則使用 { stdio: 'ignore' } 選項。
const cp = require('child_process');
通過 API 創(chuàng)建出來的子進程和父進程沒有任何必然聯(lián)系
4個異步方法,創(chuàng)建子進程:fork、exec、execFile、spawn
spawn(command, args)
:處理一些會有很多子進程 I/O 時、進程會有大量輸出時使用
execFile(file, args[, callback])
:只需執(zhí)行一個外部程序的時候使用,執(zhí)行速度快,處理用戶輸入相對安全
exec(command, options)
:想直接訪問線程的 shell 命令時使用,一定要注意用戶輸入
fork(modulePath, args)
:想將一個 Node 進程作為一個獨立的進程來運行的時候使用,使得計算處理和文件描述器脫離 Node 主進程(復制一個子進程)
Node
非 Node
3個同步方法:execSync
、execFileSync
、spawnSync
其他三種方法都是 spawn()
的延伸。
fork 方法會開放一個 IPC 通道,不同的 Node 進程進行消息傳送
一個子進程消耗 30ms 啟動時間和 10MB 內(nèi)存
記住,衍生的 Node.js 子進程獨立于父進程,但兩者之間建立的 IPC 通信通道除外。 每個進程都有自己的內(nèi)存,帶有自己的 V8 實例
舉個?
在一個目錄下新建 worker.js 和 master.js 兩個文件:
# child.js const t = JSON.parse(process.argv[2]); console.error(`子進程 t=${JSON.stringify(t)}`); process.send({hello:`兒子pid=${process.pid} 給爸爸進程pid=${process.ppid} 請安`}); process.on('message', (msg)=>{ console.error(`子進程 msg=${JSON.stringify(msg)}`); });
# parent.js const {fork} = require('child_process'); for(let i = 0; i < 3; i++){ const p = fork('./child.js', [JSON.stringify({id:1,name:1})]); p.on('message', (msg) => { console.log(`messsgae from child msg=${JSON.stringify(msg)}`, ); }); p.send({hello:`來自爸爸${process.pid} 進程id=${i}的問候`}); }
通過 node parent.js
啟動 parent.js,然后通過 ps aux | grep worker.js
查看進程的數(shù)量,我們可以發(fā)現(xiàn),理想狀況下,進程的數(shù)量等于 CPU 的核心數(shù),每個進程各自利用一個 CPU 核心。
這是經(jīng)典的 Master-Worker 模式(主從模式)
實際上,fork 進程是昂貴的,復制進程的目的是充分利用 CPU 資源,所以 NodeJS 在單線程上使用了事件驅(qū)動的方式來解決高并發(fā)的問題。
適用場景
一般用于比較耗時的場景,并且用node去實現(xiàn)的,比如下載文件;
fork可以實現(xiàn)多線程下載:將文件分成多塊,然后每個進程下載一部分,最后拼起來;
會把輸出結果緩存好,通過回調(diào)返回最后結果或者異常信息
const cp = require('child_process'); // 第一個參數(shù),要運行的可執(zhí)行文件的名稱或路徑。這里是echo cp.execFile('echo', ['hello', 'world'], (err, stdout, stderr) => { if (err) { console.error(err); } console.log('stdout: ', stdout); console.log('stderr: ', stderr); });
適用場景
比較適合開銷小的任務,更關注結果,比如ls等;
主要用來執(zhí)行一個shell方法,其內(nèi)部還是調(diào)用了spawn ,不過他有最大緩存限制。
只有一個字符串命令
和 shell 一模一樣
const cp = require('child_process'); cp.exec(`cat ${__dirname}/messy.txt | sort | uniq`, (err, stdout, stderr) => { console.log(stdout); });
適用場景
比較適合開銷小的任務,更關注結果,比如ls等;
通過流可以使用有大量數(shù)據(jù)輸出的外部應用,節(jié)約內(nèi)存
使用流提高數(shù)據(jù)響應效率
spawn 方法返回一個 I/O 的流接口
單一任務
const cp = require('child_process'); const child = cp.spawn('echo', ['hello', 'world']); child.on('error', console.error); # 輸出是流,輸出到主進程stdout,控制臺 child.stdout.pipe(process.stdout); child.stderr.pipe(process.stderr);
多任務串聯(lián)
const cp = require('child_process'); const path = require('path'); const cat = cp.spawn('cat', [path.resolve(__dirname, 'messy.txt')]); const sort = cp.spawn('sort'); const uniq = cp.spawn('uniq'); # 輸出是流 cat.stdout.pipe(sort.stdin); sort.stdout.pipe(uniq.stdin); uniq.stdout.pipe(process.stdout);
適用場景
spawn是流式的,所以適合耗時任務,比如執(zhí)行npm install,打印install的過程
在進程已結束并且子進程的標準輸入輸出流(sdtio)已關閉之后,則觸發(fā) 'close'
事件。這個事件跟exit
不同,因為多個進程可以共享同個stdio流。
參數(shù):
code(退出碼,如果子進程是自己退出的話)
signal(結束子進程的信號)
問題:code一定是有的嗎?
(從對code的注解來看好像不是)比如用kill
殺死子進程,那么,code是?
參數(shù):
code、signal,如果子進程是自己退出的,那么code
就是退出碼,否則為null;
如果子進程是通過信號結束的,那么,signal
就是結束進程的信號,否則為null。
這兩者中,一者肯定不為null。
注意事項:exit
事件觸發(fā)時,子進程的stdio stream可能還打開著。(場景?)此外,nodejs監(jiān)聽了SIGINT和SIGTERM信號,也就是說,nodejs收到這兩個信號時,不會立刻退出,而是先做一些清理的工作,然后重新拋出這兩個信號。(目測此時js可以做清理工作了,比如關閉數(shù)據(jù)庫等。)
SIGINT
:interrupt,程序終止信號,通常在用戶按下CTRL+C時發(fā)出,用來通知前臺進程終止進程。SIGTERM
:terminate,程序結束信號,該信號可以被阻塞和處理,通常用來要求程序自己正常退出。shell命令kill缺省產(chǎn)生這個信號。如果信號終止不了,我們才會嘗試SIGKILL(強制終止)。
當發(fā)生下列事情時,error就會被觸發(fā)。當error觸發(fā)時,exit可能觸發(fā),也可能不觸發(fā)。(內(nèi)心是崩潰的)
無法衍生該進程。
進程無法kill。
向子進程發(fā)送消息失敗。
當采用process.send()
來發(fā)送消息時觸發(fā)。
參數(shù):message
,為json對象,或者primitive value;sendHandle
,net.Socket對象,或者net.Server對象(熟悉cluster的同學應該對這個不陌生)
.connected:當調(diào)用.disconnected()
時,設為false。代表是否能夠從子進程接收消息,或者對子進程發(fā)送消息。
.disconnect() :關閉父進程、子進程之間的IPC通道。當這個方法被調(diào)用時,disconnect
事件就會觸發(fā)。如果子進程是node實例(通過child_process.fork()創(chuàng)建),那么在子進程內(nèi)部也可以主動調(diào)用process.disconnect()
來終止IPC通道。
應對單線程問題,通常使用多進程的方式來模擬多線程
對 cpu 利用不足
某個未捕獲的異常可能會導致整個程序的退出
Node 進程占用了 7 個線程
Node 中最核心的是 v8 引擎,在 Node 啟動后,會創(chuàng)建 v8 的實例,這個實例是多線程的
主線程:編譯、執(zhí)行代碼
編譯/優(yōu)化線程:在主線程執(zhí)行的時候,可以優(yōu)化代碼
分析器線程:記錄分析代碼運行時間,為 Crankshaft 優(yōu)化代碼執(zhí)行提供依據(jù)
垃圾回收的幾個線程
JavaScript 的執(zhí)行是單線程
的,但 Javascript 的宿主環(huán)境,無論是 Node 還是瀏覽器都是多線程的。
Javascript 為什么是單線程?
這個問題需要從瀏覽器說起,在瀏覽器環(huán)境中對于 DOM 的操作,試想如果多個線程來對同一個 DOM 操作是不是就亂了呢,那也就意味著對于DOM的操作只能是單線程,避免 DOM 渲染沖突。在瀏覽器環(huán)境中 UI 渲染線程和 JS 執(zhí)行引擎是互斥的,一方在執(zhí)行時都會導致另一方被掛起,這是由 JS 引擎所決定的。
Node 中有一些 IO 操作(DNS,F(xiàn)S)和一些 CPU 密集計算(Zlib,Crypto)會啟用 Node 的線程池
線程池默認大小為 4,可以手動更改線程池默認大小
process.env.UV_THREADPOOL_SIZE = 64
Node 10.5.0 的發(fā)布,給出了一個實驗性質(zhì)的模塊 worker_threads
給 Node 提供真正的多線程能力
worker_thread 模塊中有 4 個對象和 2 個類
isMainThread: 是否是主線程,源碼中是通過 threadId === 0 進行判斷的。
MessagePort: 用于線程之間的通信,繼承自 EventEmitter。
MessageChannel: 用于創(chuàng)建異步、雙向通信的通道實例。
threadId: 線程 ID。
Worker: 用于在主線程中創(chuàng)建子線程。第一個參數(shù)為 filename,表示子線程執(zhí)行的入口。
parentPort: 在 worker 線程里是表示父進程的 MessagePort 類型的對象,在主線程里為 null
workerData: 用于在主進程中向子進程傳遞數(shù)據(jù)(data 副本)
const { isMainThread, parentPort, workerData, threadId, MessageChannel, MessagePort, Worker } = require('worker_threads'); function mainThread() { for (let i = 0; i < 5; i++) { const worker = new Worker(__filename, { workerData: i }); worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); }); worker.on('message', msg => { console.log(`main: receive ${msg}`); worker.postMessage(msg + 1); }); } } function workerThread() { console.log(`worker: workerDate ${workerData}`); parentPort.on('message', msg => { console.log(`worker: receive ${msg}`); }), parentPort.postMessage(workerData); } if (isMainThread) { mainThread(); } else { workerThread(); }
const assert = require('assert'); const { Worker, MessageChannel, MessagePort, isMainThread, parentPort } = require('worker_threads'); if (isMainThread) { const worker = new Worker(__filename); const subChannel = new MessageChannel(); worker.postMessage({ hereIsYourPort: subChannel.port1 }, [subChannel.port1]); subChannel.port2.on('message', (value) => { console.log('received:', value); }); } else { parentPort.once('message', (value) => { assert(value.hereIsYourPort instanceof MessagePort); value.hereIsYourPort.postMessage('the worker is sending this'); value.hereIsYourPort.close(); }); }
進程是資源分配的最小單位,線程是CPU調(diào)度的最小單位
IPC (Inter-process communication) 即進程間通信
,由于每個進程創(chuàng)建之后都有自己的獨立地址空間,實現(xiàn) IPC 的目的就是為了進程之間資源共享訪問。
實現(xiàn) IPC 的方式有多種:管道、消息隊列、信號量、Domain Socket,Node.js 通過 pipe 來實現(xiàn)。
實際上,父進程會在創(chuàng)建子進程之前,會先創(chuàng)建 IPC 通道并監(jiān)聽這個 IPC,然后再創(chuàng)建子進程,通過環(huán)境變量(NODE_CHANNEL_FD)告訴子進程和 IPC 通道相關的文件描述符,子進程啟動的時候根據(jù)文件描述符連接 IPC 通道,從而和父進程建立連接。
句柄是一種可以用來標識資源的引用的,它的內(nèi)部包含了指向?qū)ο蟮奈募Y源描述符。
一般情況下,當我們想要將多個進程監(jiān)聽到一個端口下,可能會考慮使用主進程代理的方式處理:
然而,這種代理方案會導致每次請求的接收和代理轉發(fā)用掉兩個文件描述符,而系統(tǒng)的文件描述符是有限的,這種方式會影響系統(tǒng)的擴展能力。
所以,為什么要使用句柄?原因是在實際應用場景下,建立 IPC 通信后可能會涉及到比較復雜的數(shù)據(jù)處理場景,句柄可以作為 send()
方法的第二個可選參數(shù)傳入,也就是說可以直接將資源的標識通過 IPC 傳輸,避免了上面所說的代理轉發(fā)造成的文件描述符的使用。
以下是支持發(fā)送的句柄類型:
net.Socket
net.Server
net.Native
dgram.Socket
dgram.Native
父進程創(chuàng)建子進程之后,父進程退出了,但是父進程對應的一個或多個子進程還在運行,這些子進程會被系統(tǒng)的 init 進程收養(yǎng),對應的進程 ppid 為 1,這就是孤兒進程。通過以下代碼示例說明。
# worker.js const http = require('http'); const server = http.createServer((req, res) => { res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid); // 記錄當前工作進程 pid 及父進程 ppid }); let worker; process.on('message', function (message, sendHandle) { if (message === 'server') { worker = sendHandle; worker.on('connection', function(socket) { server.emit('connection', socket); }); } });
# master.js const fork = require('child_process').fork; const server = require('net').createServer(); server.listen(3000); const worker = fork('worker.js'); worker.send('server', server); console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid); process.exit(0); // 創(chuàng)建子進程之后,主進程退出,此時創(chuàng)建的 worker 進程會成為孤兒進程
控制臺進行測試,輸出當前工作進程 pid 和 父進程 ppid
由于在 master.js 里退出了父進程,活動監(jiān)視器所顯示的也就只有工作進程。
再次驗證,打開控制臺調(diào)用接口,可以看到工作進程 5611 對應的 ppid 為 1(為 init 進程),此時已經(jīng)成為了孤兒進程
守護進程運行在后臺不受終端的影響,什么意思呢?
Node.js 開發(fā)的同學們可能熟悉,當我們打開終端執(zhí)行 node app.js
開啟一個服務進程之后,這個終端就會一直被占用,如果關掉終端,服務就會斷掉,即前臺運行模式
。
如果采用守護進程進程方式,這個終端我執(zhí)行 node app.js
開啟一個服務進程之后,我還可以在這個終端上做些別的事情,且不會相互影響。
創(chuàng)建子進程
在子進程中創(chuàng)建新會話(調(diào)用系統(tǒng)函數(shù) setsid)
改變子進程工作目錄(如:“/” 或 “/usr/ 等)
父進程終止
index.js 文件里的處理邏輯使用 spawn 創(chuàng)建子進程完成了上面的第一步操作。
設置 options.detached
為 true 可以使子進程在父進程退出后繼續(xù)運行(系統(tǒng)層會調(diào)用 setsid 方法),這是第二步操作。
options.cwd 指定當前子進程工作目錄若不做設置默認繼承當前工作目錄,這是第三步操作。
運行 daemon.unref() 退出父進程,這是第四步操作。
// index.js const spawn = require('child_process').spawn; function startDaemon() { const daemon = spawn('node', ['daemon.js'], { cwd: '/usr', detached : true, stdio: 'ignore', }); console.log('守護進程開啟 父進程 pid: %s, 守護進程 pid: %s', process.pid, daemon.pid); daemon.unref(); } startDaemon()
daemon.js 文件里處理邏輯開啟一個定時器每 10 秒執(zhí)行一次,使得這個資源不會退出,同時寫入日志到子進程當前工作目錄下
/usr/daemon.js const fs = require('fs'); const { Console } = require('console'); // custom simple logger const logger = new Console(fs.createWriteStream('./stdout.log'), fs.createWriteStream('./stderr.log')); setInterval(function() { logger.log('daemon pid: ', process.pid, ', ppid: ', process.ppid); }, 1000 * 10);
在實際工作中對于守護進程并不陌生,例如 PM2、Egg-Cluster 等,以上只是一個簡單的 Demo 對守護進程做了一個說明,在實際工作中對守護進程的健壯性要求還是很高的,例如:進程的異常監(jiān)聽、工作進程管理調(diào)度、進程掛掉之后重啟等等,這些還需要去不斷思考。
目錄是什么?
進程的當前工作目錄可以通過 process.cwd()
命令獲取,默認為當前啟動的目錄,如果是創(chuàng)建子進程則繼承于父進程的目錄,可通過 process.chdir()
命令重置,例如通過 spawn 命令創(chuàng)建的子進程可以指定 cwd 選項設置子進程的工作目錄。
有什么作用?
例如,通過 fs 讀取文件,如果設置為相對路徑則相對于當前進程啟動的目錄進行查找,所以,啟動目錄設置有誤的情況下將無法得到正確的結果。還有一種情況程序里引用第三方模塊也是根據(jù)當前進程啟動的目錄來進行查找的。
// 示例 process.chdir('/Users/may/Documents/test/') // 設置當前進程目錄 console.log(process.cwd()); // 獲取當前進程目錄
到此,關于“NodeJS中的進程管理怎么實現(xiàn)”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續(xù)學習更多相關知識,請繼續(xù)關注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。