天天看點

深入了解Node.js 程序與線程面試會問程序線程Node.js 中的程序與線程

前言

程序

與 

線程

是一個程式員的必知概念,面試經常被問及,但是一些文章内容隻是講講理論知識,可能一些小夥伴并沒有真的了解,在實際開發中應用也比較少。本篇文章除了介紹概念,通過Node.js 的角度講解 

程序

與 

線程

,并且講解一些在項目中的實戰的應用,讓你不僅能迎戰面試官還可以在實戰中完美應用。

文章導覽

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

面試會問

Node.js是單線程嗎?

Node.js 做耗時的計算時候,如何避免阻塞?

Node.js如何實作多程序的開啟和關閉?

Node.js可以建立線程嗎?

你們開發過程中如何實作程序守護的?

除了使用第三方子產品,你們自己是否封裝過一個多程序架構?

程序

程序 

Process

是計算機中的程式關于某資料集合上的一次運作活動,是系統進行資源配置設定和排程的基本機關,是作業系統結構的基礎,程序是線程的容器(來自百科)。程序是資源配置設定的最小機關。我們啟動一個服務、運作一個執行個體,就是開一個服務程序,例如 Java 裡的 JVM 本身就是一個程序,Node.js 裡通過 

node app.js

 開啟一個服務程序,多程序就是程序的複制(fork),fork 出來的每個程序都擁有自己的獨立空間位址、資料棧,一個程序無法通路另外一個程序裡定義的變量、資料結構,隻有建立了 IPC 通信,程序之間才可資料共享。

  • Node.js開啟服務程序例子
  1. const http = require('http');

  2. const server = http.createServer();

  3. server.listen(3000,()=>{

  4. process.title='程式員成長指北測試程序';

  5. console.log('程序id',process.pid)

  6. })

運作上面代碼後,以下為 Mac 系統自帶的監控工具 “活動螢幕” 所展示的效果,可以看到我們剛開啟的 Nodejs 程序 7663

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

線程

線程是作業系統能夠進行運算排程的最小機關,首先我們要清楚線程是隸屬于程序的,被包含于程序之中。一個線程隻能隸屬于一個程序,但是一個程序是可以擁有多個線程的。

單線程

單線程就是一個程序隻開一個線程

Javascript 就是屬于單線程,程式順序執行(這裡暫且不提JS異步),可以想象一下隊列,前面一個執行完之後,後面才可以執行,當你在使用單線程語言編碼時切勿有過多耗時的同步操作,否則線程會造成阻塞,導緻後續響應無法處理。你如果采用 Javascript 進行編碼時候,請盡可能的利用Javascript異步操作的特性。

經典計算耗時造成線程阻塞的例子

  1. const http = require('http');

  2. const longComputation = () => {

  3. let sum = 0;

  4. for (let i = 0; i < 1e10; i++) {

  5. sum += i;

  6. };

  7. return sum;

  8. };

  9. const server = http.createServer();

  10. server.on('request', (req, res) => {

  11. if (req.url === '/compute') {

  12. console.info('計算開始',new Date());

  13. const sum = longComputation();

  14. console.info('計算結束',new Date());

  15. return res.end(`Sum is ${sum}`);

  16. } else {

  17. res.end('Ok')

  18. }

  19. });

  20. server.listen(3000);

  21. //列印結果

  22. //計算開始 2019-07-28T07:08:49.849Z

  23. //計算結束 2019-07-28T07:09:04.522Z

檢視列印結果,當我們調用 

127.0.0.1:3000/compute

的時候,如果想要調用其他的路由位址比如127.0.0.1/大約需要15秒時間,也可以說一個使用者請求完第一個 

compute

接口後需要等待15秒,這對于使用者來說是極其不友好的。下文我會通過建立多程序的方式 

child_process.fork

 和 

cluster

 來解決解決這個問題。

單線程的一些說明

  • Node.js 雖然是單線程模型,但是其基于事件驅動、異步非阻塞模式,可以應用于高并發場景,避免了線程建立、線程之間上下文切換所産生的資源開銷。
  • 當你的項目中需要有大量計算,CPU 耗時的操作時候,要注意考慮開啟多程序來完成了。
  • Node.js 開發過程中,錯誤會引起整個應用退出,應用的健壯性值得考驗,尤其是錯誤的異常抛出,以及程序守護是必須要做的。
  • 單線程無法利用多核CPU,但是後來Node.js 提供的API以及一些第三方工具相應都得到了解決,文章後面都會講到。

Node.js 中的程序與線程

Node.js 是 Javascript 在服務端的運作環境,建構在 chrome 的 V8 引擎之上,基于事件驅動、非阻塞I/O模型,充分利用作業系統提供的異步 I/O 進行多任務的執行,适合于 I/O 密集型的應用場景,因為異步,程式無需阻塞等待結果傳回,而是基于回調通知的機制,原本同步模式等待的時間,則可以用來處理其它任務,

科普:在 Web 伺服器方面,著名的 Nginx 也是采用此模式(事件驅動),避免了多線程的線程建立、線程上下文切換的開銷,Nginx 采用 C 語言進行編寫,主要用來做高性能的 Web 伺服器,不适合做業務。

Web業務開發中,如果你有高并發應用場景那麼 Node.js 會是你不錯的選擇。

在單核 CPU 系統之上我們采用 單程序 + 單線程 的模式來開發。在多核 CPU 系統之上,可以通過 

child_process.fork

 開啟多個程序(Node.js 在 v0.8 版本之後新增了Cluster 來實作多程序架構) ,即 多程序 + 單線程 模式。注意:開啟多程序不是為了解決高并發,主要是解決了單程序模式下 Node.js CPU 使用率不足的情況,充分利用多核 CPU 的性能。

Node.js 中的程序

process 子產品

Node.js 中的程序 Process 是一個全局對象,無需 require 直接使用,給我們提供了目前程序中的相關資訊。官方文檔提供了詳細的說明,感興趣的可以親自實踐下 Process 文檔。

  • process.env

    :環境變量,例如通過 

    process.env.NODE_ENV

     擷取不同環境項目配置資訊
  • process.nextTick

    :這個在談及 

    EventLoop

     時經常為會提到
  • process.pid

    :擷取目前程序id
  • process.ppid

    :目前程序對應的父程序
  • process.cwd()

    :擷取目前程序工作目錄,
  • process.platform

    :擷取目前程序運作的作業系統平台
  • process.uptime()

    :目前程序已運作時間,例如:pm2 守護程序的 uptime 值
  • 程序事件: 

    process.on(‘uncaughtException’,cb)

     捕獲異常資訊、 

    process.on(‘exit’,cb)

    程序推出監聽
  • 三個标準流: 

    process.stdout

     标準輸出、 

    process.stdin

     标準輸入、 

    process.stderr

     标準錯誤輸出
  • process.title

     指定程序名稱,有的時候需要給程序指定一個名稱

以上僅列舉了部分常用到功能點,除了 Process 之外 Node.js 還提供了 child_process 子產品用來對子程序進行操作,在下文 Nodejs程序建立會繼續講述。

Node.js 程序建立

程序建立有多種方式,本篇文章以child_process子產品和cluster子產品進行講解。

child_process子產品

child_process 是 Node.js 的内置子產品,官網位址:

childprocess 官網位址:http://nodejs.cn/api/childprocess.html#childprocesschild_process

幾個常用函數:四種方式

  • child_process.spawn()

    :适用于傳回大量資料,例如圖像處理,二進制資料處理。
  • child_process.exec()

    :适用于小量資料,maxBuffer 預設值為 200 * 1024 超出這個預設值将會導緻程式崩潰,資料量過大可采用 spawn。
  • child_process.execFile()

    :類似 

    child_process.exec()

    ,差別是不能通過 shell 來執行,不支援像 I/O 重定向和檔案查找這樣的行為
  • child_process.fork()

    :衍生新的程序,程序之間是互相獨立的,每個程序都有自己的 V8 執行個體、記憶體,系統資源是有限的,不建議衍生太多的子程序出來,通長根據系統* CPU 核心數*設定。
CPU 核心數這裡特别說明下,fork 确實可以開啟多個程序,但是并不建議衍生出來太多的程序,cpu核心數的擷取方式 

constcpus=require('os').cpus();

,這裡 cpus 傳回一個對象數組,包含所安裝的每個 CPU/核心的資訊,二者總和的數組哦。假設主機裝有兩個cpu,每個cpu有4個核,那麼總核數就是8。

fork開啟子程序 Demo

fork開啟子程序解決文章起初的計算耗時造成線程阻塞。在進行 compute 計算時建立子程序,子程序計算完成通過 

send

 方法将結果發送給主程序,主程序通過 

message

 監聽到資訊後處理并退出。

fork_app.js
  1. const http = require('http');

  2. const fork = require('child_process').fork;

  3. const server = http.createServer((req, res) => {

  4. if(req.url == '/compute'){

  5. const compute = fork('./fork_compute.js');

  6. compute.send('開啟一個新的子程序');

  7. // 當一個子程序使用 process.send() 發送消息時會觸發 'message' 事件

  8. compute.on('message', sum => {

  9. res.end(`Sum is ${sum}`);

  10. compute.kill();

  11. });

  12. // 子程序監聽到一些錯誤消息退出

  13. compute.on('close', (code, signal) => {

  14. console.log(`收到close事件,子程序收到信号 ${signal} 而終止,退出碼 ${code}`);

  15. compute.kill();

  16. })

  17. }else{

  18. res.end(`ok`);

  19. }

  20. });

  21. server.listen(3000, 127.0.0.1, () => {

  22. console.log(`server started at http://${127.0.0.1}:${3000}`);

  23. });

fork_compute.js

針對文初需要進行計算的的例子我們建立子程序拆分出來單獨進行運算。

  1. const computation = () => {

  2. let sum = 0;

  3. console.info('計算開始');

  4. console.time('計算耗時');

  5. for (let i = 0; i < 1e10; i++) {

  6. sum += i

  7. };

  8. console.info('計算結束');

  9. console.timeEnd('計算耗時');

  10. return sum;

  11. };

  12. process.on('message', msg => {

  13. console.log(msg, 'process.pid', process.pid); // 子程序id

  14. const sum = computation();

  15. // 如果Node.js程序是通過程序間通信産生的,那麼,process.send()方法可以用來給父程序發送消息

  16. process.send(sum);

  17. })

cluster子產品

cluster 開啟子程序Demo

  1. const http = require('http');

  2. const numCPUs = require('os').cpus().length;

  3. const cluster = require('cluster');

  4. if(cluster.isMaster){

  5. console.log('Master proces id is',process.pid);

  6. // fork workers

  7. for(let i= 0;i<numCPUs;i++){

  8. cluster.fork();

  9. }

  10. cluster.on('exit',function(worker,code,signal){

  11. console.log('worker process died,id',worker.process.pid)

  12. })

  13. }else{

  14. // Worker可以共享同一個TCP連接配接

  15. // 這裡是一個http伺服器

  16. http.createServer(function(req,res){

  17. res.writeHead(200);

  18. res.end('hello word');

  19. }).listen(8000);

  20. }

cluster原理分析

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

cluster子產品調用fork方法來建立子程序,該方法與child_process中的fork是同一個方法。cluster子產品采用的是經典的主從模型,Cluster會建立一個master,然後根據你指定的數量複制出多個子程序,可以使用 

cluster.isMaster

屬性判斷目前程序是master還是worker(工作程序)。由master程序來管理所有的子程序,主程序不負責具體的任務處理,主要工作是負責排程和管理。

cluster子產品使用内置的負載均衡來更好地處理線程之間的壓力,該負載均衡使用了 

Round-robin

算法(也被稱之為循環算法)。當使用Round-robin排程政策時,master accepts()所有傳入的連接配接請求,然後将相應的TCP請求處理發送給選中的工作程序(該方式仍然通過IPC來進行通信)。

開啟多程序時候端口疑問講解:如果多個Node程序監聽同一個端口時會出現 

Error:listen EADDRIUNS

的錯誤,而cluster子產品為什麼可以讓多個子程序監聽同一個端口呢?原因是master程序内部啟動了一個TCP伺服器,而真正監聽端口的隻有這個伺服器,當來自前端的請求觸發伺服器的connection事件後,master會将對應的socket具柄發送給子程序。

child_process 子產品與cluster 子產品總結

無論是 child_process 子產品還是 cluster 子產品,為了解決 Node.js 執行個體單線程運作,無法利用多核 CPU 的問題而出現的。核心就是父程序(即 master 程序)負責監聽端口,接收到新的請求後将其分發給下面的 worker 程序。

cluster子產品的一個弊端:

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

cluster内部隐時的建構TCP伺服器的方式來說對使用者确實簡單和透明了很多,但是這種方式無法像使用childprocess那樣靈活,因為一直主程序隻能管理一組相同的工作程序,而自行通過childprocess來建立工作程序,一個主程序可以控制多組程序。原因是child_process操作子程序時,可以隐式的建立多個TCP伺服器,對比上面的兩幅圖應該能了解我說的内容。

Node.js程序通信原理

前面講解的無論是child_process子產品,還是cluster子產品,都需要主程序和工作程序之間的通信。通過fork()或者其他API,建立了子程序之後,為了實作父子程序之間的通信,父子程序之間才能通過message和send()傳遞資訊。

IPC這個詞我想大家并不陌生,不管那一張開發語言隻要提到程序通信,都會提到它。IPC的全稱是Inter-Process Communication,即程序間通信。它的目的是為了讓不同的程序能夠互相通路資源并進行協調工作。實作程序間通信的技術有很多,如命名管道,匿名管道,socket,信号量,共享記憶體,消息隊列等。Node中實作IPC通道是依賴于libuv。windows下由命名管道(name pipe)實作,*nix系統則采用Unix Domain Socket實作。表現在應用層上的程序間通信隻有簡單的message事件和send()方法,接口十分簡潔和消息化。

IPC建立和實作示意圖

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

IPC通信管道是如何建立的

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

父程序在實際建立子程序之前,會建立 

IPC通道

并監聽它,然後才 

真正的

建立出 

子程序

,這個過程中也會通過環境變量(NODECHANNELFD)告訴子程序這個IPC通道的檔案描述符。子程序在啟動的過程中,根據檔案描述符去連接配接這個已存在的IPC通道,進而完成父子程序之間的連接配接。

Node.js句柄傳遞

講句柄之前,先想一個問題,send句柄發送的時候,真的是将伺服器對象發送給了子程序?

子程序對象send()方法可以發送的句柄類型

  • net.Socket TCP套接字
  • net.Server TCP伺服器,任意建立在TCP服務上的應用層服務都可以享受它帶來的好處
  • net.Native C++層面的TCP套接字或IPC管道
  • dgram.Socket UDP套接字
  • dgram.Native C++層面的UDP套接字

send句柄發送原理分析

結合句柄的發送與還原示意圖更容易了解。

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

send()

方法在将消息發送到IPC管道前,實際将消息組裝成了兩個對象,一個參數是hadler,另一個是message。message參數如下所示:

  1. {

  2. cmd:'NODE_HANDLE',

  3. type:'net.Server',

  4. msg:message

  5. }

發送到IPC管道中的實際上是我們要發送的句柄檔案描述符。這個message對象在寫入到IPC管道時,也會通過 

JSON.stringfy()

進行序列化。是以最終發送到IPC通道中的資訊都是字元串,send()方法能發送消息和句柄并不意味着它能發送任何對象。

連接配接了IPC通道的子線程可以讀取父程序發來的消息,将字元串通過JSON.parse()解析還原為對象後,才觸發message事件将消息傳遞給應用層使用。在這個過程中,消息對象還要被進行過濾處理,message.cmd的值如果以NODE為字首,它将響應一個内部事件internalMessage,如果message.cmd值為NODEHANDLE,它将取出 

message.type

值和得到的檔案描述符一起還原出一個對應的對象。

以發送的TCP伺服器句柄為例,子程序收到消息後的還原過程代碼如下:

  1. function(message,handle,emit){

  2. var self = this;

  3. var server = new net.Server();

  4. server.listen(handler,function(){

  5. emit(server);

  6. });

  7. }

這段還原代碼, 

子程序根據message.type建立對應的TCP伺服器對象,然後監聽到檔案描述符上

。由于底層細節不被應用層感覺,是以子程序中,開發者會有一種伺服器對象就是從父程序中直接傳遞過來的錯覺。

Node程序之間隻有消息傳遞,不會真正的傳遞對象,這種錯覺是抽象封裝的結果。目前Node隻支援我前面提到的幾種句柄,并非任意類型的句柄都能在程序之間傳遞,除非它有完整的發送和還原的過程。

Node.js多程序架構模型

我們自己實作一個多程序架構守護Demo

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

編寫主程序

master.js 主要處理以下邏輯:

  • 建立一個 server 并監聽 3000 端口。
  • 根據系統 cpus 開啟多個子程序
  • 通過子程序對象的 send 方法發送消息到子程序進行通信
  • 在主程序中監聽了子程序的變化,如果是自殺信号重新啟動一個工作程序。
  • 主程序在監聽到退出消息的時候,先退出子程序在退出主程序
  1. // master.js

  2. const fork = require('child_process').fork;

  3. const cpus = require('os').cpus();

  4. const server = require('net').createServer();

  5. server.listen(3000);

  6. process.title = 'node-master'

  7. const workers = {};

  8. const createWorker = () => {

  9. const worker = fork('worker.js')

  10. worker.on('message', function (message) {

  11. if (message.act === 'suicide') {

  12. createWorker();

  13. }

  14. })

  15. worker.on('exit', function(code, signal) {

  16. console.log('worker process exited, code: %s signal: %s', code, signal);

  17. delete workers[worker.pid];

  18. });

  19. worker.send('server', server);

  20. workers[worker.pid] = worker;

  21. console.log('worker process created, pid: %s ppid: %s', worker.pid, process.pid);

  22. }

  23. for (let i=0; i<cpus.length; i++) {

  24. createWorker();

  25. }

  26. process.once('SIGINT', close.bind(this, 'SIGINT')); // kill(2) Ctrl-C

  27. process.once('SIGQUIT', close.bind(this, 'SIGQUIT')); // kill(3) Ctrl-\

  28. process.once('SIGTERM', close.bind(this, 'SIGTERM')); // kill(15) default

  29. process.once('exit', close.bind(this));

  30. function close (code) {

  31. console.log('程序退出!', code);

  32. if (code !== 0) {

  33. for (let pid in workers) {

  34. console.log('master process exited, kill worker pid: ', pid);

  35. workers[pid].kill('SIGINT');

  36. }

  37. }

  38. process.exit(0);

  39. }

工作程序

worker.js 子程序處理邏輯如下:

  • 建立一個 server 對象,注意這裡最開始并沒有監聽 3000 端口
  • 通過 message 事件接收主程序 send 方法發送的消息
  • 監聽 uncaughtException 事件,捕獲未處理的異常,發送自殺資訊由主程序重建程序,子程序在連結關閉之後退出
  1. // worker.js

  2. const http = require('http');

  3. const server = http.createServer((req, res) => {

  4. res.writeHead(200, {

  5. 'Content-Type': 'text/plan'

  6. });

  7. res.end('I am worker, pid: ' + process.pid + ', ppid: ' + process.ppid);

  8. throw new Error('worker process exception!'); // 測試異常程序退出、重新開機

  9. });

  10. let worker;

  11. process.title = 'node-worker'

  12. process.on('message', function (message, sendHandle) {

  13. if (message === 'server') {

  14. worker = sendHandle;

  15. worker.on('connection', function(socket) {

  16. server.emit('connection', socket);

  17. });

  18. }

  19. });

  20. process.on('uncaughtException', function (err) {

  21. console.log(err);

  22. process.send({act: 'suicide'});

  23. worker.close(function () {

  24. process.exit(1);

  25. })

  26. })

Node.js 程序守護

什麼是程序守護?

每次啟動 Node.js 程式都需要在指令視窗輸入指令 

node app.js

 才能啟動,但如果把指令視窗關閉則Node.js 程式服務就會立刻斷掉。除此之外,當我們這個 Node.js 服務意外崩潰了就不能自動重新開機程序了。這些現象都不是我們想要看到的,是以需要通過某些方式來守護這個開啟的程序,執行 node app.js 開啟一個服務程序之後,我還可以在這個終端上做些别的事情,且不會互相影響。,當出現問題可以自動重新開機。

如何實作程序守護

這裡我隻說一些第三方的程序守護架構,pm2 和 forever ,它們都可以實作程序守護,底層也都是通過上面講的 child_process 子產品和 cluster 子產品 實作的,這裡就不再提它們的原理。

pm2 指定生産環境啟動一個名為 test 的 node 服務

pm2 start app.js --env production --name test
           

pm2常用api

  • pm2 stopName/processID

     停止某個服務,通過服務名稱或者服務程序ID
  • pm2deleteName/processID

     删除某個服務,通過服務名稱或者服務程序ID
  • pm2 logs[Name]

     檢視日志,如果添加服務名稱,則指定檢視某個服務的日志,不加則檢視所有日志
  • pm2 start app.js-i4

     叢集,-i參數用來告訴PM2以clustermode的形式運作你的app(對應的叫forkmode),後面的數字表示要啟動的工作線程的數量。如果給定的數字為0,PM2則會根據你CPU核心的數量來生成對應的工作線程。注意一般在生産環境使用cluster_mode模式,測試或者本地環境一般使用fork模式,友善測試到錯誤。
  • pm2 reloadNamepm2 restartName

     應用程式代碼有更新,可以用重載來加載新代碼,也可以用重新開機來完成,reload可以做到0秒當機加載新的代碼,restart則是重新啟動,生産環境中多用reload來完成代碼更新!
  • pm2 showName

     檢視服務詳情
  • pm2 list

     檢視pm2中所有項目
  • pm2 monit

    用monit可以打開實時螢幕去檢視資源占用情況

pm2 官網位址:

http://pm2.keymetrics.io/docs/usage/quick-start/

forever 就不特殊說明了,官網位址

https://github.com/foreverjs/forever

注意:二者更推薦pm2,看一下二者對比就知道我為什麼更推薦使用pm2了。https://www.jianshu.com/p/fdc12d82b661

linux 關閉一個程序

  • 查找與程序相關的PID号

    ps aux | grep server

    說明:

root 20158  0.0  5.0 1251592 95396 ?   Sl   5月17   1:19 node /srv/mini-program-api/launch_pm2.js
           
上面是執行指令後在linux中顯示的結果,第二個參數就是程序對應的PID
           
  • 殺死程序
  1. 以優雅的方式結束程序

    kill -l PID

    -l選項告訴kill指令用好像啟動程序的使用者已登出的方式結束程序。當使用該選項時,kill指令也試圖殺死所留下的子程序。但這個指令也不是總能成功--或許仍然需要先手工殺死子程序,然後再殺死父程序。

  2. kill 指令用于終止程序

    例如:

    kill-9[PID]

    -9 表示強迫程序立即停止

    這個強大和危險的指令迫使程序在運作時突然終止,程序在結束後不能自我清理。危害是導緻系統資源無法正常釋放,一般不推薦使用,除非其他辦法都無效。當使用此指令時,一定要通過ps -ef确認沒有剩下任何僵屍程序。隻能通過終止父程序來消除僵屍程序。如果僵屍程序被init收養,問題就比較嚴重了。殺死init程序意味着關閉系統。如果系統中有僵屍程序,并且其父程序是init, 而且僵屍程序占用了大量的系統資源,那麼就需要在某個時候重新開機機器以清除程序表了。

  3. killall指令

    殺死同一程序組内的所有程序。其允許指定要終止的程序的名稱,而非PID。

    killall httd

Node.js 線程

Node.js關于單線程的誤區

  1. const http = require('http');

  2. const server = http.createServer();

  3. server.listen(3000,()=>{

  4. process.title='程式員成長指北測試程序';

  5. console.log('程序id',process.pid)

  6. })

仍然看本文第一段代碼,建立了http服務,開啟了一個程序,都說了Node.js是單線程,是以 Node 啟動後線程數應該為 1,但是為什麼會開啟7個線程呢?難道Javascript不是單線程不知道小夥伴們有沒有這個疑問?

解釋一下這個原因:

Node 中最核心的是 v8 引擎,在 Node 啟動後,會建立 v8 的執行個體,這個執行個體是多線程的。

  • 主線程:編譯、執行代碼。
  • 編譯/優化線程:在主線程執行的時候,可以優化代碼。
  • 分析器線程:記錄分析代碼運作時間,為 Crankshaft 優化代碼執行提供依據。
  • 垃圾回收的幾個線程。

是以大家常說的 Node 是單線程的指的是 JavaScript 的執行是單線程的(開發者編寫的代碼運作在單線程環境中),但 Javascript 的宿主環境,無論是 Node 還是浏覽器都是多線程的因為libuv中有線程池的概念存在的,libuv會通過類似線程池的實作來模拟不同作業系統的異步調用,這對開發者來說是不可見的。

某些異步 IO 會占用額外的線程

還是上面那個例子,我們在定時器執行的同時,去讀一個檔案:

  1. const fs = require('fs')

  2. setInterval(() => {

  3. console.log(new Date().getTime())

  4. }, 3000)

  5. fs.readFile('./index.html', () => {})

線程數量變成了 11 個,這是因為在 Node 中有一些 IO 操作(DNS,FS)和一些 CPU 密集計算(Zlib,Crypto)會啟用 Node 的線程池,而線程池預設大小為 4,因為線程數變成了 11。我們可以手動更改線程池預設大小:

process.env.UV_THREADPOOL_SIZE = 64
           

一行代碼輕松把線程變成 71。

Libuv

Libuv 是一個跨平台的異步IO庫,它結合了UNIX下的libev和Windows下的IOCP的特性,最早由Node的作者開發,專門為Node提供多平台下的異步IO支援。Libuv本身是由C++語言實作的,Node中的非蘇塞IO以及事件循環的底層機制都是由libuv實作的。

libuv架構圖

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

在Window環境下,libuv直接使用Windows的IOCP來實作異步IO。在非Windows環境下,libuv使用多線程來模拟異步IO。

注意下面我要說的話,Node的異步調用是由libuv來支援的,以上面的讀取檔案的例子,讀檔案實質的系統調用是由libuv來完成的,Node隻是負責調用libuv的接口,等資料傳回後再執行對應的回調方法。

Node.js 線程建立

直到 Node 10.5.0 的釋出,官方才給出了一個實驗性質的子產品 worker_threads 給 Node 提供真正的多線程能力。

先看下簡單的 demo:

  1. const {

  2. isMainThread,

  3. parentPort,

  4. workerData,

  5. threadId,

  6. MessageChannel,

  7. MessagePort,

  8. Worker

  9. } = require('worker_threads');

  10. function mainThread() {

  11. for (let i = 0; i < 5; i++) {

  12. const worker = new Worker(__filename, { workerData: i });

  13. worker.on('exit', code => { console.log(`main: worker stopped with exit code ${code}`); });

  14. worker.on('message', msg => {

  15. console.log(`main: receive ${msg}`);

  16. worker.postMessage(msg + 1);

  17. });

  18. }

  19. }

  20. function workerThread() {

  21. console.log(`worker: workerDate ${workerData}`);

  22. parentPort.on('message', msg => {

  23. console.log(`worker: receive ${msg}`);

  24. }),

  25. parentPort.postMessage(workerData);

  26. }

  27. if (isMainThread) {

  28. mainThread();

  29. } else {

  30. workerThread();

  31. }

上述代碼在主線程中開啟五個子線程,并且主線程向子線程發送簡單的消息。

由于 worker_thread 目前仍然處于實驗階段,是以啟動時需要增加 --experimental-worker flag,運作後觀察活動螢幕,開啟了5個子線程

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

worker_thread 子產品

workerthread 核心代碼(位址https://github.com/nodejs/node/blob/master/lib/workerthreads.js) worker_thread 子產品中有 4 個對象和 2 個類,可以自己去看上面的源碼。

  • isMainThread: 是否是主線程,源碼中是通過 threadId === 0 進行判斷的。
  • MessagePort: 用于線程之間的通信,繼承自 EventEmitter。
  • MessageChannel: 用于建立異步、雙向通信的通道執行個體。
  • threadId: 線程 ID。
  • Worker: 用于在主線程中建立子線程。第一個參數為 filename,表示子線程執行的入口。
  • parentPort: 在 worker 線程裡是表示父程序的 MessagePort 類型的對象,在主線程裡為 null
  • workerData: 用于在主程序中向子程序傳遞資料(data 副本)

總結

多程序 vs 多線程

對比一下多線程與多程序:

屬性 多程序 多線程 比較
資料 資料共享複雜,需要用IPC;資料是分開的,同步簡單 因為共享程序資料,資料共享簡單,同步複雜 各有千秋
CPU、記憶體 占用記憶體多,切換複雜,CPU使用率低 占用記憶體少,切換簡單,CPU使用率高 多線程更好
銷毀、切換 建立銷毀、切換複雜,速度慢 建立銷毀、切換簡單,速度很快 多線程更好
coding 編碼簡單、調試友善 編碼、調試複雜 編碼、調試複雜
可靠性 程序獨立運作,不會互相影響 線程同呼吸共命運 多程序更好
分布式 可用于多機多核分布式,易于擴充 隻能用于多核分布式 多程序更好
深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程

我知道你在看

深入了解Node.js 程式與線程面試會問程式線程Node.js 中的程式與線程
看完三件事:點贊 + 評論 + 關注

系列文章: 

Node.js 進階進階之 fs 檔案子產品學習

來,告訴你Node.js究竟是什麼?

Node進階-探究不在V8堆記憶體中存儲的Buffer對象

說Node.js做後端開發,stream有必要了解下

深入了解Javacript從作用域作用域鍊開始

【JS必知必會】高階函數詳解與實戰