Nodejs是非阻塞的,源于它是基于事件循環的設計模式,該模式也稱為Reactor模式。
Nodejs同時也是單線程的,這里的單線程指的是開發人員編寫的代碼運行在單線程上,而Nodejs的內部一些實現代碼卻是多線程的,如對于I/O 的處理(讀取文件、網絡請求等)。
但對于I/O請求不也是開發人員編寫的代碼嗎,不是說我們自己寫的代碼都是運行在單線程上的,怎么這里又可能變成多線程了? 這里就要講到reactor模式了。在此之前,先簡單了解下Blocking I/O與Non-blocking I/O。
Blocking I/O
Blocking I/O是程序會等待I/O請求直到結果返回,相當于控制權一直在等待I/O這邊,在等待的這段時間里程序不會去干其他事,就這么一直干等著。例子如:
data = socket.read();
// wait until the data fetch back
print(data)
對于web server來說,是必須要處理多個請求的。對于Blocking I/O情況,是無法處理多個請求,每個請求都會在上一個請求處理完才能處理。解決的方法是啟用多線程處理。
開啟多個線程處理的代價有點高(內存占用,上下文切換),而且從圖中看到每個線程都有很多空余時間在干等著,無法充分利用時間。
Non-blocking I/O
對于Non-blocking I/O, 一般是請求后直接返回,不用等待請求結果返回。如果沒有數據可以返回的話,是直接返回一個預設好的常量標識當前還沒數據可以返回。
這里首先舉例一個最基本的實現方式,不斷循環這些資源直到能讀取到數據。
// 資源集合
resources = [socketA, socketB, pipeA];
// 只要還有資源沒獲取到數據,就一直循環操作
while(!resources.isEmpty()) {
for(i = 0; i < resources.length; i++) {
resource = resources[i];
// 直接返回non-blocking
// 若無數據則直接返回預設常量
let data = resource.read();
if(data === NO_DATA_AVAILABLE)
// 該資源還在等待中未準備好
continue;
if(data === RESOURCE_CLOSED)
// 該資源已經讀取完畢,從集合中刪除
resources.remove(i);
else
// 數據已經獲取,處理數據
consumeData(data);
}
}
這樣就可以做到單個線程中處理并發處理多個請求資源了。這種做法被稱為busy-wait,該做法雖然使得單個線程可以處理多個并發請求,但CPU會一直消耗在輪詢中,無法抽身去干其他事情。因此non-blocking I/O一般通過synchronous event demultiplexer來實現。
關于什么是synchronous event demultiplexer,這里引用wikipedia中的一段話。
Uses an event loop to block on all resources. The demultiplexer sends the resource to the dispatcher when it is possible to start a synchronous operation on a resource without blocking
(Example: a synchronous call to read() will block if there is no data to read. The demultiplexer uses select() on the resource, which blocks until the resource is available for reading. In this case, a synchronous call to read() won't block, and the demultiplexer can send the resource to the dispatcher.)
簡單來說就是,對于事件循環中的資源會通過該多路分發器(demultiplexer)下發給對應的程序去處理,處理好了則把對應事件保存到event queue中等待事件循環輪詢運行。
如上述例子說的調用read()之后馬上可以運行接下來的代碼而不會產生阻塞,阻塞的事情交給了分發器去做了,具體怎么做每個系統有不同的實現,這就是更底層的事了。
簡單例子如:
socketA, pipeB;
// 注冊事件
watchedList.add(socketA, FOR_READ);
watchedList.add(pipeB, FOR_READ);
// demultiplexer blocking 等待事件完成(成功取回數據)
// events保存成功的事件
while(events = demultiplexer.watch(watchedList)) {
...
}
Reactor Pattern
Nodejs中的事件循環正是基于event demultiplexer和event queue,而這兩塊正是Reactor Pattern的核心。
1. event demultiplexer接收到I/O請求然后下發給對應的底層去處理。
2. 一旦I/O獲取到了數據,event demultiplexer會把注冊的回調函數添加到event queue中等待event loop去執行。
3. event queue中的回調函數依次被event loop執行,直到event queue為空。
4. 當event queue中沒數據了或者event demultiplexer沒有再接受到請求,程序即event loop就會結束,意味著該應用就退出了,否則回到第一步。
Event Demultiplexer
event demultiplexer實際上是一個抽象的概念,不同的系統有不同的實現方式,如Linux的epoll,MacOS中的kqueue,Windows中的IOCP。nodejs則通過libuv屏蔽了對不同系統的實現支持跨平臺,提供了針對多種不同I/O請求的具體處理方式的API(如File I/O,Network I/O,DNS處理等)。
可以認為libuv把這一堆復雜的東西都結合在一起形成了nodejs中的event demultiplexer。
libuv中,對于一些I/O操作是直接利用系統層級I/O中的non-blocking和asynchronous特性(如提到的epoll等),但對于一些類型的I/O,由于復雜性的問題libuv則通過thread pool來處理。
所以就如同一開始說的,用戶開發層面的代碼是單線程的,但在I/O處理中是有可能出現多線程,但不會涉及到開發人員寫的JS代碼,因為thread pool是在libuv庫里面的。
Event Queue
上面說到了event queue,是用來存儲回調函數等待被event loop處理的。但實際上,不止一個event queue隊列,事件循環要處理的主要有4個類型的隊列。
Timers and Intervals Queue: 保存setTimeout和setInterval中的回調函數(實際上不是隊列,數據結構是最小堆實現,這里就統一都叫隊列了)
IO Event Queue: 保存已經完成的I/O回調函數。
Immediates Queue: 保存setImmediate中的回調函數。
Close Handlers Queue: 其他所有close事件的回調,如socket.on('close', ...)。
除了上述四個主要隊列外,還有兩個比較特殊的隊列:
Next Ticks Queue:保存process.nextTick中的回調函數。
Other Microtasks Queue:保存Promise等microtask中的回調函數。
參考