Node.js 的原理總結

全棧者發表於2019-07-08

一. nodejs背景

先來說說nodejs最常被提到的幾個關鍵詞,“單執行緒”,“非阻塞非同步IO”,“事件迴圈”。接下來主要來通過這幾個關鍵字總結一下nodejs的內在原理,以及引申出的一些問題。

二. nodejs是單執行緒嗎?

如果說nodejs是單執行緒語言,可以想象一下,一個單例項的nodejs的伺服器同時接受100個使用者請求時,第100個使用者的請求要等前面99的使用者處理完成才能得到處理,如果每個使用者的請求要0.3秒,第100個使用者需要30秒的等待,這顯然和我們的實際情況並不符合,所以說,nodejs並不是單純的單執行緒。

那為什麼說nodejs是單執行緒語言呢?而是因為nodejs中javascript程式碼的執行是單執行緒,怎麼理解這句話,看下面程式碼。

console.log('javascript start');
setTimeout(()=>{
  console.log('javascript setTimeout');
}, 2000);

const now = Date.now();
while(Date.now() < now + 4000) {}
console.log('javascript end');
複製程式碼

執行結果:

$ node index.js 
javascript start
javascript end
javascript setTimeout
複製程式碼

上面的程式碼中,setTimeout的回撥程式碼在while執行4秒期間,計時器已經是過了兩秒的,而'javascript setTimeout'這一句列印卻在'javascript end'之後,即使計時器在兩秒後回撥程式碼應該被執行時,因為javascript的執行緒處於非空閒狀態,而不能輸出'javascript setTimeout',javascript程式碼是單執行緒這樣理解。

三. nodejs的非同步IO

再拿上面的例子來看,當100個使用者請求同時被接受到時,當需要IO(網路IO/檔案IO)操作時,單執行緒的javascript並不會停下來等待IO操作完成,而是“事件驅動”開始介入,javascript執行執行緒繼續執行未完的javascript程式碼,當執行完成後該執行緒處於空閒狀態,可以看下面這一段程式碼示例。

// http.js

const http = require('http');
const fs = require('fs');

let num = 0;

http.createServer((req, res) => {
  console.log('request id: %d, time:', num++, Date.now());
  fs.readFile('./test.txt', ()=> {
    res.end('response');
  });
}).listen(9007, ()=>{
  console.log('server start, 127.0.0.1:9007');
});
複製程式碼
// req.js
const http = require('http');

for(let i=0; i<100; i++) {
  http.get('http://127.0.0.1:9007', (res)=>{
    res.on("data",(data)=>{
      console.log('response time:', Date.now())
      // console.log('data', data.toString())
    })
  }).on('error', (err)=>{
    console.log('error', err);
  })
}
複製程式碼
node http.js     // 啟動伺服器
複製程式碼

Node.js 的原理總結

node req.js    // 發起100個請求
複製程式碼

Node.js 的原理總結

可以看出100個請求均是在請求返回之前非常短的時間都被得到了處理,而返回則均在請求之後,並非請求按接收順序依次等待各個IO得到處理後依次返回。

四. 事件迴圈

說到事件迴圈,在上面的請求中,100個請求的都在非常短的時間得到了處理,而後請求又各自得到了回覆,可以思考一下,javascript已經執行到了第100個請求,而第1個請求才得到回覆,而第一個請求的棧資訊沒有丟失,說明第一個請求的請求棧資訊被記錄了,這一過程便是註冊IO事件。

從上面註冊事件後,事件迴圈得到啟用,對於上面程式碼中fs.readFile這個讀檔案IO則開始真正執行,而這時候IO的執行跟javascript程式碼的執行便沒有關係了,由nodejs底層libuv提供的執行緒池接收該檔案IO執行工作,該執行緒池預設大小為4,可以通過環境變數process.env.UV_THREADPOOL_SIZE在啟動的時候進行調整,但是最大不能超過1024個,有興趣的可以檢視執行緒池原始碼;由上可以看出nodejs內部實際是多程式並行工作的,而是利用事件迴圈做了封口處理。

nodesys.png

再來說說事件迴圈,上面示例中fs.readFile讀檔案時,如何知道這個讀操作完成了呢?可以思考一下,讀操作是執行緒池來控制執行的,在該執行緒執行前,先在註冊事件的記憶體中初始化一個狀態是“執行中”,並且事件迴圈也已經被啟用,開始輪詢等待執行結果,當執行IO的執行緒在執行完之後,再通過底層的非同步IO介面(epoll_wait/IOCP)進行通知到初始註冊的任務佇列記憶體進行變更狀態,事件迴圈輪詢到狀態變成“已完成”,這時候在IO事件註冊時注入的回撥函式得到執行權,javascript執行緒開始工作,整個非同步過程完畢。

Node.js 的原理總結
可以看看事件迴圈裡面都要經過哪些步驟,如何稱為事件迴圈:
Node.js 的原理總結

可以看一下英文原版的解釋,事件迴圈解釋

Node.js 的原理總結

翻譯過來:

**階段概覽**
timers:這個階段執行setTimeout() 和 setInterval()中到期的回撥函式
I/O callbacks:執行所有除了setTimeout() ,setInterval(),close事件,setImmediate的其他回撥函式
idle, prepare:僅內部使用
poll:獲取新的I/O 事件,在適當的條件下nodejs會阻塞在這個階段
check:setImmediate的回撥函式在這裡被呼叫
close callbacks:像socket.on("close",func)這一類執行close事件的回撥

複製程式碼

如上內容均為自己總結,難免會有錯誤或者認識偏差,如有問題,希望大家留言指正,以免誤人,若有什麼問題請留言,會盡力回答之。如果對你有幫助不要忘了分享給你的朋友哦!也可以關注作者,檢視歷史文章並且關注最新動態,助你早日成為一名全棧工程師!

Node.js 的原理總結

相關文章