node.js極速入門課程:進入學習
我們先來看幾個常見的說法
在具體分析這幾個說法是不是、為什麼之前,我們先來做一些準備工作
多程序web應用範例虛擬碼
listenFd = new Socket(); // 建立監聽socket
Bind(listenFd, 80); // 繫結埠
Listen(listenFd); // 開始監聽
for ( ; ; ) {
// 接收使用者端請求,通過新的socket建立連線
connFd = Accept(listenFd);
// fork子程序
if ((pid = Fork()) === 0) {
// 子程序中
// BIO讀取網路請求資料,阻塞,發生程序排程
request = connFd.read();
// BIO讀取本地檔案,阻塞,發生程序排程
content = ReadFile('test.txt');
// 將檔案內容寫入響應
Response.write(content);
}
}
登入後複製
多執行緒應用實際上和多程序類似,只不過將一個請求分配一個程序換成了一個請求分配一個執行緒。執行緒對比程序更輕量,在系統資源佔用上更少,上下文切換(ps:所謂上下文切換,稍微解釋一下:單核心CPU的情況下同一時間只能執行一個程序或執行緒中的任務,而為了宏觀上的並行,則需要在多個程序或執行緒之間按時間片來回切換以保證各進、執行緒都有機會被執行)的開銷也更小;同時執行緒間更容易共用記憶體,便於開發
上文中提到了web應用的兩個核心要點,一個是進(線)程模型,一個是I/O模型。那阻塞I/O到底是什麼?又有哪些其他的I/O模型呢?彆著急,首先我們看一下什麼是阻塞
簡而言之,阻塞是指函數呼叫返回之前,當前進(線)程會被掛起,進入等待狀態,在這個狀態下,當前進(線)程暫停執行,引起CPU的進(線)程排程。函數只有在內部工作全部執行完成後才會返回給呼叫者
所以阻塞I/O是,應用程式通過API呼叫I/O操作後,當前進(線)程將會進入等待狀態,程式碼無法繼續往下執行,這時CPU可以進行進(線)程排程,即切換到其他可執行的進(線)程繼續執行,當前進(線)程在底層I/O請求處理完後才會返回並可以繼續執行
在瞭解了什麼是阻塞和阻塞I/O後,我們來分析一下傳統web應用多進(線)程 + 阻塞I/O模型有什麼弊端。
因為一個請求需要分配一個進(線)程,這樣的系統在並行量大時需要維護大量進(線)程,且需要進行大量的上下文切換,這都需要大量的CPU、記憶體等系統資源支撐,所以在高並行請求進來時CPU和記憶體開銷會急劇上升,可能會迅速拖垮整個系統導致服務不可用
接下來我們看看nodejs應用是如何實現的。
const net = require('net');
const server = net.createServer();
const fs = require('fs');
server.listen(80); // 監聽埠
// 監聽事件建立連線
server.on('connection', (socket) => {
// 監聽事件讀取請求資料
socket.on('data', (data) => {
// 非同步讀取本地檔案
fs.readFile('test.txt', (err, data) => {
// 將讀取的內容寫入響應
socket.write(data);
socket.end();
})
});
});
登入後複製
可以看到在nodejs中,我們可以以非同步的方式去進行I/O操作,通過API呼叫I/O操作後會馬上返回,緊接著就可以繼續執行其他程式碼邏輯,那為什麼nodejs中的I/O是「非阻塞」的呢?回答這個問題之前我們再做一些準備工作,參考nodejs進階視訊講解:進入學習
首先看下一個read操作需要經歷哪些步驟
接下來我們看一下作業系統中有哪些I/O模型
阻塞式I/O
非阻塞式I/O
I/O多路複用(程序可同時監聽多個I/O裝置就緒)
訊號驅動I/O
非同步I/O
那麼nodejs裡到底使用了哪種I/O模型呢?是上圖中的「非阻塞I/O」嗎?彆著急,先接著往下看,我們來了解下nodejs的體系結構
最上面一層是就是我們編寫nodejs應用程式碼時可以使用的API庫,下面一層則是用來打通nodejs和它所依賴的底層庫的一箇中間層,比如實現讓js程式碼可以呼叫底層的c程式碼庫。來到最下面一層,可以看到前端同學熟悉的V8,還有其他一些底層依賴。注意,這裡有一個叫libuv的庫,它是幹什麼的呢?從圖中也能看出,libuv幫助nodejs實現了底層的執行緒池、非同步I/O等功能。libuv實際上是一個跨平臺的c語言庫,它在windows、linux等不同平臺下會呼叫不同的實現。我這裡主要分析linux下libuv的實現,因為我們的應用大部分時候還是執行在linux環境下的,且平臺間的差異性並不會影響我們對nodejs原理的分析和理解。好了,對於nodejs在linux下的I/O模型來說,libuv實際上提供了兩種不同場景下的不同實現,處理網路I/O主要由epoll函數實現(其實就是I/O多路複用,在前面的圖中使用的是select函數來實現I/O多路複用,而epoll可以理解為select函數的升級版,這個暫時不做具體分析),而處理檔案I/O則由多執行緒(執行緒池) + 阻塞I/O模擬非同步I/O實現
下面是一段我寫的nodejs底層實現的虛擬碼幫助大家理解
listenFd = new Socket(); // 建立監聽socket
Bind(listenFd, 80); // 繫結埠
Listen(listenFd); // 開始監聽
for ( ; ; ) {
// 阻塞在epoll函數上,等待網路資料準備好
// epoll可同時監聽listenFd以及多個使用者端連線上是否有資料準備就緒
// clients表示當前所有使用者端連線,curFd表示epoll函數最終拿到的一個就緒的連線
curFd = Epoll(listenFd, clients);
if (curFd === listenFd) {
// 監聽通訊端收到新的使用者端連線,建立通訊端
int connFd = Accept(listenFd);
// 將新建的連線新增到epoll監聽的list
clients.push(connFd);
}
else {
// 某個使用者端連線資料就緒,讀取請求資料
request = curFd.read();
// 這裡拿到請求資料後可以發出data事件進入nodejs的事件迴圈
...
}
}
// 讀取本地檔案時,libuv用多執行緒(執行緒池) + BIO模擬非同步I/O
ThreadPool.run((callback) => {
// 線上程裡用BIO讀取檔案
String content = Read('text.txt');
// 發出事件呼叫nodejs提供的callback
});
登入後複製
通過I/O多路複用 + 多執行緒模擬的非同步I/O配合事件迴圈機制,nodejs就實現了單執行緒處理並行請求並且不會阻塞。所以回到之前所說的「非阻塞I/O」模型,實際上nodejs並沒有直接使用通常定義上的非阻塞I/O模型,而是I/O多路複用模型 + 多執行緒BIO。我認為「非阻塞I/O」其實更多是對nodejs程式設計人員來說的一種描述,從編碼方式和程式碼執行順序上來講,nodejs的I/O呼叫的確是「非阻塞」的
至此我們應該可以瞭解到,nodejs的I/O模型其實主要是由I/O多路複用和多執行緒下的阻塞I/O兩種方式一起組成的,而應對高並行請求時發揮作用的主要就是I/O多路複用。好了,那最後我們來總結一下nodejs執行緒模型和I/O模型對比傳統web應用多進(線)程 + 阻塞I/O模型的優勢和劣勢
更多node相關知識,請存取:!
以上就是淺析Node高並行的原理的詳細內容,更多請關注TW511.COM其它相關文章!