Nodejs是非阻塞的,源於它是基於事件循環的設計模式,該模式也稱爲Reactor模式。javascript
Nodejs同時也是單線程的,這裏的單線程指的是開發人員編寫的代碼運行在單線程上,而Nodejs的內部一些實現代碼倒是多線程的,如對於I/O 的處理(讀取文件、網絡請求等)。關於Event Loop
在另外一篇文章中有粗略提到,本文將詳細闡述。java
但對於I/O請求不也是開發人員編寫的代碼嗎,不是說咱們本身寫的代碼都是運行在單線程上的,怎麼這裏又可能變成多線程了? 這裏就要講到reactor模式了。在此以前,先簡單瞭解下Blocking I/O
與Non-blocking I/O
。node
Blocking I/Oreact
Blocking I/O
是程序會等待I/O請求直到結果返回,至關於控制權一直在等待I/O這邊,在等待的這段時間裏程序不會去幹其餘事,就這麼一直乾等着。例子如:web
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)) {
...
}
複製代碼
Nodejs中的事件循環正是基於event demultiplexer
和event queue
,而這兩塊正是Reactor Pattern的核心。對於Nodejs的事件循環,首選要明確的一點是:
只有一個主線程執行JS代碼,咱們寫的代碼就是在該線程執行的,該線程也同是
event loop
運行的線程。(並非主線程運行JS代碼,而後又有一個線程在同時運行event loop
)。
該模式執行過程大體以下圖所示:
event demultiplexer
接收到I/O請求而後下發給對應的底層去處理。
一旦I/O獲取到了數據,event demultiplexer
會把註冊的回調函數添加到event queue
中等待event loop
去執行。
event queue
中的回調函數依次被event loop
執行,直到event queue
爲空。
當event queue
中沒數據了或者event demultiplexer
沒有再接受到請求,程序即event loop
就會結束,意味着該應用就退出了,不然回到第一步。
以前已經初略講過了Event Demultiplexer
是什麼了,這裏詳細講下nodejs中的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結構以下圖所示:
libuv中,對於一些I/O操做是直接利用系統層級I/O中的non-blocking
和asynchronous
特性(如提到的epoll等),但對於一些類型的I/O,因爲複雜性的問題libuv則經過thread pool來處理。
因此就如同一開始說的,用戶開發層面的代碼是單線程的,但在I/O處理中是有可能出現多線程,但不會涉及到開發人員寫的JS代碼,由於thread pool是在libuv庫裏面的。
上面說到了event queue
,是用來存儲回調函數等待被event loop
處理的。但實際上,不止一個event queue
隊列,事件循環要處理的主要有4個類型的隊列。
setTimeout
和setInterval
中的回調函數(實際上不是隊列,數據結構是最小堆實現,這裏就統一都叫隊列了)setImmediate
中的回調函數。close
事件的回調,如socket.on('close', ...)
。除了上述四個主要隊列外,還有兩個比較特殊的隊列:
process.nextTick
中的回調函數。Promise
等microtask中的回調函數。這裏又再插一句,macrotask和microtask的區別。
那麼這些隊列是怎麼被事件循環處理的呢?直接看圖。
事件循環會依次處理timers and intervals queue
,IO event queue
,immediates queue
,close handlers queue
這四個隊列,若是處理完close hanlers queue
後,timers and intervals
沒有數據再進來,就退出事件循環。
處理其中一個隊列的過程稱爲一個phase。一次事件循環就是處理這四個phase的過程。那另外兩個特殊的隊列是在何時運行的呢? 答案就是在每一個 phase運行完後立刻就檢查這兩個隊列有無數據,有的話就立刻執行這兩個隊列中的數據直至隊列爲空。當這兩個隊列都爲空時,event loop 就會接着執行下一個phase。
這兩個隊列相比,Next Ticks Queue
的權限要比Other Microtasks Queue
的權限要高,所以Next Ticks Queue
會先執行。
此外要注意的是,若是process.nextTick
中出現遞歸調用沒有中止條件的話,Next Ticks Queue
將一直有數據進來一直都不會爲空,則會阻塞event loop
的執行。爲了防止該狀況,process.maxTickDepth
定義了迭代的最大值,不過從NodeJS v0.12版本開始已經移除了。
參考
1.Event Loop and the Big Picture
2.What you should know to really understand the Node.js Event Loop