欧美亚洲中文,在线国自产视频,欧洲一区在线观看视频,亚洲综合中文字幕在线观看

      1. <dfn id="rfwes"></dfn>
          <object id="rfwes"></object>
        1. 站長(zhǎng)資訊網(wǎng)
          最全最豐富的資訊網(wǎng)站

          聊聊Node.js中的多進(jìn)程和多線程

          大家都知道 Node 是單線程的,卻不知它也提供了多進(jìn)(線)程模塊來(lái)加速處理一些特殊任務(wù),本文便帶領(lǐng)大家了解下 Node.js 的多進(jìn)(線)程,希望對(duì)大家有所幫助!

          聊聊Node.js中的多進(jìn)程和多線程

          我們都知道 Node.js 采用的是單線程、基于事件驅(qū)動(dòng)的異步 I/O 模型,其特性決定了它無(wú)法利用 CPU 多核的優(yōu)勢(shì),也不善于完成一些非 I/O 類型的操作(比如執(zhí)行腳本、AI 計(jì)算、圖像處理等),為了解決此類問題,Node.js 提供了常規(guī)的多進(jìn)(線程)方案(關(guān)于進(jìn)程、線程的討論,可參見筆者的另一篇文章 Node.js 與并發(fā)模型),本文便為大家介紹 Node.js 的多進(jìn)(線)程機(jī)制。

          child_process

          我們可使用 child_process 模塊創(chuàng)建 Node.js 的子進(jìn)程,來(lái)完成一些特殊的任務(wù)(比如執(zhí)行腳本),該模塊主要提供了 execexecFile、forkspwan 等方法,下面我們就簡(jiǎn)單介紹下這些方法的使用。

          exec

          const { exec } = require('child_process');  exec('ls -al', (error, stdout, stderr) => {   console.log(stdout); });

          該方法根據(jù) options.shell 指定的可執(zhí)行文件處理命令字符串,在命令的執(zhí)行過(guò)程中緩存其輸出,直到命令執(zhí)行完成后,再將執(zhí)行結(jié)果以回調(diào)函數(shù)參數(shù)的形式返回。

          該方法的參數(shù)解釋如下:

          • command:將要執(zhí)行的命令(比如 ls -al);

          • options:參數(shù)設(shè)置(可不指定),相關(guān)屬性如下:

            • cwd:子進(jìn)程的當(dāng)前工作目錄,默認(rèn)取 process.cwd() 的值;

            • env:環(huán)境變量設(shè)置(為鍵值對(duì)對(duì)象),默認(rèn)取 process.env 的值;

            • encoding:字符編碼,默認(rèn)值為:utf8;

            • shell:處理命令字符串的可執(zhí)行文件,Unix 上默認(rèn)值為 /bin/sh,Windows 上默認(rèn)值取 process.env.ComSpec 的值(如為空則為 cmd.exe);比如:

              const { exec } = require('child_process');  exec("print('Hello World!')", { shell: 'python' }, (error, stdout, stderr) => {   console.log(stdout); });

              運(yùn)行上面的例子將輸出 Hello World!,這等同于子進(jìn)程執(zhí)行了 python -c "print('Hello World!')" 命令,因此在使用該屬性時(shí)需要注意,所指定的可執(zhí)行文件必須支持通過(guò) -c 選項(xiàng)來(lái)執(zhí)行相關(guān)語(yǔ)句。

              注:碰巧 Node.js 也支持 -c 選項(xiàng),但它等同于 --check 選項(xiàng),只用來(lái)檢測(cè)指定的腳本是否存在語(yǔ)法錯(cuò)誤,并不會(huì)執(zhí)行相關(guān)腳本。

            • signal:使用指定的 AbortSignal 終止子進(jìn)程,該屬性在 v14.17.0 以上可用,比如:

              const { exec } = require('child_process');  const ac = new AbortController(); exec('ls -al', { signal: ac.signal }, (error, stdout, stderr) => {});

              上例中,我們可通過(guò)調(diào)用 ac.abort() 來(lái)提前終止子進(jìn)程。

            • timeout:子進(jìn)程的超時(shí)時(shí)間(如果該屬性的值大于 0,那么當(dāng)子進(jìn)程運(yùn)行時(shí)間超過(guò)指定值時(shí),將會(huì)給子進(jìn)程發(fā)送屬性 killSignal 指定的終止信號(hào)),單位毫米,默認(rèn)值為 0

            • maxBuffer:stdout 或 stderr 所允許的最大緩存(二進(jìn)制),如果超出,子進(jìn)程將會(huì)被殺死,并且將會(huì)截?cái)嗳魏屋敵?,默認(rèn)值為 1024 * 1024;

            • killSignal:子進(jìn)程終止信號(hào),默認(rèn)值為 SIGTERM

            • uid:執(zhí)行子進(jìn)程的 uid;

            • gid:執(zhí)行子進(jìn)程的 gid;

            • windowsHide:是否隱藏子進(jìn)程的控制臺(tái)窗口,常用于 Windows 系統(tǒng),默認(rèn)值為 false;

          • callback:回調(diào)函數(shù),包含 error、stdoutstderr 三個(gè)參數(shù):

            • error:如果命令行執(zhí)行成功,值為 null,否則值為 Error 的一個(gè)實(shí)例,其中 error.code 為子進(jìn)程的退出的錯(cuò)誤碼,error.signal 為子進(jìn)程終止的信號(hào);
            • stdoutstderr:子進(jìn)程的 stdoutstderr,按照 encoding 屬性的值進(jìn)行編碼,如果 encoding 的值為 buffer,或者 stdout、stderr 的值是一個(gè)無(wú)法識(shí)別的字符串,將按照 buffer 進(jìn)行編碼。

          execFile

          const { execFile } = require('child_process');  execFile('ls', ['-al'], (error, stdout, stderr) => {   console.log(stdout); });

          該方法的功能類似于 exec,唯一的區(qū)別是 execFile 在默認(rèn)情況下直接用指定的可執(zhí)行文件(即參數(shù) file 的值)處理命令,這使得其效率略高于 exec(如果查看 shell 的處理邏輯,筆者感覺這效率可忽略不計(jì))。

          該方法的參數(shù)解釋如下:

          • file:可執(zhí)行文件的名字或路徑;

          • args:可執(zhí)行文件的參數(shù)列表;

          • options:參數(shù)設(shè)置(可不指定),相關(guān)屬性如下:

            • shell:值為 false 時(shí)表示直接用指定的可執(zhí)行文件(即參數(shù) file 的值)處理命令,值為 true 或其它字符串時(shí),作用等同于 exec 中的 shell,默認(rèn)值為 false;
            • windowsVerbatimArguments:在 Windows 中是否對(duì)參數(shù)進(jìn)行引號(hào)或轉(zhuǎn)義處理,在 Unix 中將忽略該屬性,默認(rèn)值為 false
            • 屬性 cwd、env、encoding、timeout、maxBuffer、killSignal、uidgid、windowsHidesignal 在上文中已介紹,此處不再重述。
          • callback:回調(diào)函數(shù),等同于 exec 中的 callback,此處不再闡述。

          fork

          const { fork } = require('child_process');  const echo = fork('./echo.js', {   silent: true }); echo.stdout.on('data', (data) => {   console.log(`stdout: ${data}`); });  echo.stderr.on('data', (data) => {   console.error(`stderr: ${data}`); });  echo.on('close', (code) => {   console.log(`child process exited with code ${code}`); });

          該方法用于創(chuàng)建新的 Node.js 實(shí)例以執(zhí)行指定的 Node.js 腳本,與父進(jìn)程之間以 IPC 方式進(jìn)行通信。

          該方法的參數(shù)解釋如下:

          • modulePath:要運(yùn)行的 Node.js 腳本路徑;

          • args:傳遞給 Node.js 腳本的參數(shù)列表;

          • options:參數(shù)設(shè)置(可不指定),相關(guān)屬性如:

            • detached:參見下文對(duì) spwanoptions.detached 的說(shuō)明;

            • execPath:創(chuàng)建子進(jìn)程的可執(zhí)行文件;

            • execArgv:傳遞給可執(zhí)行文件的字符串參數(shù)列表,默認(rèn)取 process.execArgv 的值;

            • serialization:進(jìn)程間消息的序列號(hào)類型,可用值為 jsonadvanced,默認(rèn)值為 json;

            • slient: 如果為 true,子進(jìn)程的 stdin、stdoutstderr 將通過(guò)管道傳遞給父進(jìn)程,否則將繼承父進(jìn)程的 stdin、stdoutstderr;默認(rèn)值為 false;

            • stdio:參見下文對(duì) spwanoptions.stdio 的說(shuō)明。這里需要注意的是:

              • 如果指定了該屬性,將忽略 slient 的值;
              • 必須包含一個(gè)值為 ipc 的選項(xiàng)(比如 [0, 1, 2, 'ipc']),否則將拋出異常。
            • 屬性 cwd、env、uid、gid、windowsVerbatimArgumentssignal、timeout、killSignal 在上文中已介紹,此處不再重述。

          spwan

          const { spawn } = require('child_process');  const ls = spawn('ls', ['-al']); ls.stdout.on('data', (data) => {   console.log(`stdout: ${data}`); });  ls.stderr.on('data', (data) => {   console.error(`stderr: ${data}`); });  ls.on('close', (code) => {   console.log(`child process exited with code ${code}`); });

          該方法為 child_process 模塊的基礎(chǔ)方法,exec、execFile、fork 最終都會(huì)調(diào)用 spawn 來(lái)創(chuàng)建子進(jìn)程。

          該方法的參數(shù)解釋如下:

          • command:可執(zhí)行文件的名字或路徑;

          • args:傳遞給可執(zhí)行文件的參數(shù)列表;

          • options:參數(shù)設(shè)置(可不指定),相關(guān)屬性如下:

            • argv0:發(fā)送給子進(jìn)程 argv[0] 的值,默認(rèn)取參數(shù) command 的值;

            • detached:是否允許子進(jìn)程可以獨(dú)立于父進(jìn)程運(yùn)行(即父進(jìn)程退出后,子進(jìn)程可以繼續(xù)運(yùn)行),默認(rèn)值為 false,其值為 true 時(shí),各平臺(tái)的效果如下所述:

              • Windows 系統(tǒng)中,父進(jìn)程退出后,子進(jìn)程可以繼續(xù)運(yùn)行,并且子進(jìn)程擁有自己的控制臺(tái)窗口(該特性一旦啟動(dòng)后,在運(yùn)行過(guò)程中將無(wú)法更改);
              • 在非 Windows 系統(tǒng)中,子進(jìn)程將作為新進(jìn)程會(huì)話組的組長(zhǎng),此刻不管子進(jìn)程是否與父進(jìn)程分離,子進(jìn)程都可以在父進(jìn)程退出后繼續(xù)運(yùn)行。

              需要注意的是,如果子進(jìn)程需要執(zhí)行長(zhǎng)時(shí)間的任務(wù),并且想要父進(jìn)程提前退出,需要同時(shí)滿足以下幾點(diǎn):

              • 調(diào)用子進(jìn)程的 unref 方法從而將子進(jìn)程從父進(jìn)程的事件循環(huán)中剔除;
              • detached 設(shè)置為 true;
              • stdioignore。

              比如下面的例子:

              // hello.js const fs = require('fs'); let index = 0; function run() {   setTimeout(() => {     fs.writeFileSync('./hello', `index: ${index}`);     if (index < 10) {       index += 1;       run();     }   }, 1000); } run();  // main.js const { spawn } = require('child_process'); const child = spawn('node', ['./hello.js'], {   detached: true,   stdio: 'ignore' }); child.unref();
            • stdio:子進(jìn)程標(biāo)準(zhǔn)輸入輸出配置,默認(rèn)值為 pipe,值為字符串或數(shù)組:

              • 值為字符串時(shí),會(huì)將其轉(zhuǎn)換為含有三個(gè)項(xiàng)的數(shù)組(比如 pipe 被轉(zhuǎn)換為 ['pipe', 'pipe', 'pipe']),可用值為 pipe、overlappedignore、inherit
              • 值為數(shù)組時(shí),其中數(shù)組的前三項(xiàng)分別代表對(duì) stdinstdoutstderr 的配置,每一項(xiàng)的可用值為 pipeoverlapped、ignore、inherit、ipc、Stream 對(duì)象、正整數(shù)(在父進(jìn)程打開的文件描述符)、null(如位于數(shù)組的前三項(xiàng),等同于 pipe,否則等同于 ignore)、undefined(如位于數(shù)組的前三項(xiàng),等同于 pipe,否則等同于 ignore)。
            • 屬性 cwdenv、uid、gidserialization、shell(值為 booleanstring)、windowsVerbatimArguments、windowsHide、signal、timeout、killSignal 在上文中已介紹,此處不再重述。

          小結(jié)

          上文對(duì) child_process 模塊中主要方法的使用進(jìn)行了簡(jiǎn)短介紹,由于 execSync、execFileSync、forkSync、spwanSync 方法是 execexecFile、spwan 的同步版本,其參數(shù)并無(wú)任何差異,故不再重述。

          cluster

          通過(guò) cluster 模塊我們可以創(chuàng)建 Node.js 進(jìn)程集群,通過(guò) Node.js 進(jìn)程進(jìn)群,我們可以更加充分地利用多核的優(yōu)勢(shì),將程序任務(wù)分發(fā)到不同的進(jìn)程中以提高程序的執(zhí)行效率;下面將通過(guò)例子為大家介紹 cluster 模塊的使用:

          const http = require('http'); const cluster = require('cluster'); const numCPUs = require('os').cpus().length;  if (cluster.isPrimary) {   for (let i = 0; i < numCPUs; i++) {     cluster.fork();   } } else {   http.createServer((req, res) => {     res.writeHead(200);     res.end(`${process.pid}n`);   }).listen(8000); }

          上例通過(guò) cluster.isPrimary 屬性判斷(即判斷當(dāng)前進(jìn)程是否為主進(jìn)程)將其分為兩個(gè)部分:

          • 為真時(shí),根據(jù) CPU 內(nèi)核的數(shù)量并通過(guò) cluster.fork 調(diào)用來(lái)創(chuàng)建相應(yīng)數(shù)量的子進(jìn)程;
          • 為假時(shí),創(chuàng)建一個(gè) HTTP server,并且每個(gè) HTTP server 都監(jiān)聽同一個(gè)端口(此處為 8000)。

          運(yùn)行上面的例子,并在瀏覽器中訪問 http://localhost:8000/,我們會(huì)發(fā)現(xiàn)每次訪問返回的 pid 都不一樣,這說(shuō)明了請(qǐng)求確實(shí)被分發(fā)到了各個(gè)子進(jìn)程。Node.js 默認(rèn)采用的負(fù)載均衡策略是輪詢調(diào)度,可通過(guò)環(huán)境變量 NODE_CLUSTER_SCHED_POLICYcluster.schedulingPolicy 屬性來(lái)修改其負(fù)載均衡策略:

          NODE_CLUSTER_SCHED_POLICY = rr // 或 none  cluster.schedulingPolicy = cluster.SCHED_RR; // 或 cluster.SCHED_NONE

          另外需要注意的是,雖然每個(gè)子進(jìn)程都創(chuàng)建了 HTTP server,并都監(jiān)聽了同一個(gè)端口,但并不代表由這些子進(jìn)程自由競(jìng)爭(zhēng)用戶請(qǐng)求,因?yàn)檫@樣無(wú)法保證所有子進(jìn)程的負(fù)載達(dá)到均衡。所以正確的流程應(yīng)該是由主進(jìn)程監(jiān)聽端口,然后將用戶請(qǐng)求根據(jù)分發(fā)策略轉(zhuǎn)發(fā)到具體的子進(jìn)程進(jìn)行處理。

          由于進(jìn)程之間是相互隔離的,因此進(jìn)程之間一般通過(guò)共享內(nèi)存、消息傳遞、管道等機(jī)制進(jìn)行通訊。Node.js 則是通過(guò)消息傳遞來(lái)完成父子進(jìn)程之間的通信,比如下面的例子:

          const http = require('http'); const cluster = require('cluster'); const numCPUs = require('os').cpus().length;  if (cluster.isPrimary) {   for (let i = 0; i < numCPUs; i++) {     const worker = cluster.fork();     worker.on('message', (message) => {       console.log(`I am primary(${process.pid}), I got message from worker: "${message}"`);       worker.send(`Send message to worker`)     });   } } else {   process.on('message', (message) => {     console.log(`I am worker(${process.pid}), I got message from primary: "${message}"`)   });   http.createServer((req, res) => {     res.writeHead(200);     res.end(`${process.pid}n`);     process.send('Send message to primary');   }).listen(8000); }

          運(yùn)行上面的例子,并訪問 http://localhost:8000/,再查看終端,我們會(huì)看到類似下面的輸出:

          I am primary(44460), I got message from worker: "Send message to primary" I am worker(44461), I got message from primary: "Send message to worker" I am primary(44460), I got message from worker: "Send message to primary" I am worker(44462), I got message from primary: "Send message to worker"

          利用該機(jī)制,我們可以監(jiān)聽各子進(jìn)程的狀態(tài),以便在某個(gè)子進(jìn)程出現(xiàn)意外后,能夠及時(shí)對(duì)其進(jìn)行干預(yù),以保證服務(wù)的可用性。

          cluster 模塊的接口非常簡(jiǎn)單,為了節(jié)省篇幅,這里只對(duì) cluster.setupPrimary 方法做一些特別聲明,其它方法請(qǐng)查看官方文檔:

          • cluster.setupPrimary 調(diào)用后,相關(guān)設(shè)置將同步到在 cluster.settings 屬性中,并且每次調(diào)用都基于當(dāng)前 cluster.settings 屬性的值;
          • cluster.setupPrimary 調(diào)用后,對(duì)已運(yùn)行的子進(jìn)程沒有影響,只影響后續(xù)的 cluster.fork 調(diào)用;
          • cluster.setupPrimary 調(diào)用后,不影響后續(xù)傳遞給 cluster.fork 調(diào)用的 env 參數(shù);
          • cluster.setupPrimary 只能在主進(jìn)程中使用。

          worker_threads

          前文我們對(duì) cluster 模塊進(jìn)行了介紹,通過(guò)它我們可以創(chuàng)建 Node.js 進(jìn)程集群以提高程序的運(yùn)行效率,但 cluster 基于多進(jìn)程模型,進(jìn)程間高成本的切換以及進(jìn)程間資源的隔離,會(huì)隨著子進(jìn)程數(shù)量的增加,很容易導(dǎo)致因系統(tǒng)資源緊張而無(wú)法響應(yīng)的問題。為解決此類問題,Node.js 提供了 worker_threads,下面我們通過(guò)具體的例子對(duì)該模塊的使用進(jìn)行簡(jiǎn)單介紹:

          // server.js const http = require('http'); const { Worker } = require('worker_threads');  http.createServer((req, res) => {   const httpWorker = new Worker('./http_worker.js');   httpWorker.on('message', (result) => {     res.writeHead(200);     res.end(`${result}n`);   });   httpWorker.postMessage('Tom'); }).listen(8000);  // http_worker.js const { parentPort } = require('worker_threads');  parentPort.on('message', (name) => {   parentPort.postMessage(`Welcone ${name}!`); });

          上例展示了 worker_threads 的簡(jiǎn)單使用,在使用 worker_threads 的過(guò)程中,需要注意以下幾點(diǎn):

          • 通過(guò) worker_threads.Worker 創(chuàng)建 Worker 實(shí)例,其中 Worker 腳本既可以為一個(gè)獨(dú)立的 JavaScript 文件,也可以為字符串,比如上例可修改為:

            const code = "const { parentPort } = require('worker_threads'); parentPort.on('message', (name) => {parentPort.postMessage(`Welcone ${name}!`);})"; const httpWorker = new Worker(code, { eval: true });
          • 通過(guò) worker_threads.Worker 創(chuàng)建 Worker 實(shí)例時(shí),可以通過(guò)指定 workerData 的值來(lái)設(shè)置 Worker 子線程的初始元數(shù)據(jù),比如:

            // server.js const { Worker } = require('worker_threads'); const httpWorker = new Worker('./http_worker.js', { workerData: { name: 'Tom'} });  // http_worker.js const { workerData } = require('worker_threads'); console.log(workerData);
          • 通過(guò) worker_threads.Worker 創(chuàng)建 Worker 實(shí)例時(shí),可通過(guò)設(shè)置 SHARE_ENV 以實(shí)現(xiàn)在 Worker 子線程與主線程之間共享環(huán)境變量的需求,比如:

            const { Worker, SHARE_ENV } = require('worker_threads'); const worker = new Worker('process.env.SET_IN_WORKER = "foo"', { eval: true, env: SHARE_ENV }); worker.on('exit', () => {   console.log(process.env.SET_IN_WORKER); });
          • 不同于 cluster 中進(jìn)程間的通信機(jī)制,worker_threads 采用的 MessageChannel 來(lái)進(jìn)行線程間的通信:

            • Worker 子線程通過(guò) parentPort.postMessage 方法發(fā)送消息給主線程,并通過(guò)監(jiān)聽 parentPortmessage 事件來(lái)處理來(lái)自主線程的消息;
            • 主線程通過(guò) Worker 子線程實(shí)例(此處為 httpWorker,以下均以此代替 Worker 子線程)的 postMessage 方法發(fā)送消息給 httpWorker,并通過(guò)監(jiān)聽 httpWorkermessage 事件來(lái)處理來(lái)自 Worker 子線程的消息。

          在 Node.js 中,無(wú)論是 cluster 創(chuàng)建的子進(jìn)程,還是 worker_threads 創(chuàng)建的 Worker 子線程,它們都擁有屬于自己的 V8 實(shí)例以及事件循環(huán),所不同的是:

          • 子進(jìn)程之間的內(nèi)存空間是互相隔離的,而 Worker 子線程共享所屬進(jìn)程的內(nèi)存空間;
          • 子進(jìn)程之間的切換成本要遠(yuǎn)遠(yuǎn)高于 Worker 子線程之間的切換成本。

          盡管看起來(lái) Worker 子線程比子進(jìn)程更高效,但 Worker 子線程也有不足的地方,即cluster 提供了負(fù)載均衡,而 worker_threads 則需要我們自行完成負(fù)載均衡的設(shè)計(jì)與實(shí)現(xiàn)。

          總結(jié)

          本文介紹了 Node.js 中 child_process、clusterworker_threads 三個(gè)模塊的使用,通過(guò)這三個(gè)模塊,我們可以充分利用 CPU 多核的優(yōu)勢(shì),并以多進(jìn)(線)程的模式來(lái)高效地解決一些特殊任務(wù)(比如 AI、圖片處理等)的運(yùn)行效率。每個(gè)模塊都有其適用的場(chǎng)景,文中僅對(duì)其基本使用進(jìn)行了說(shuō)明,如何結(jié)合自己的問題進(jìn)行高效地運(yùn)用,還需要大家自行摸索。最后,本文若有紕漏之處,還望大家能夠指正,祝大家快樂編碼每一天。

          贊(0)
          分享到: 更多 (0)
          網(wǎng)站地圖   滬ICP備18035694號(hào)-2    滬公網(wǎng)安備31011702889846號(hào)