在單線程的情況下,NodeJs是如何分發子任務去執行的?

對於大多數非NodeJs的應用程序來說,在同時處理多任務的時候,通常是開啟多個子線程去處理子任務;但是對於NodeJs來說,因為他本身是單線程的,那麼他還能支配什麼子單位去做那些子任務? 其實可以這樣問,NodeJs支配去做子任務的「子單位」究竟是什麼?


JavaScript確實是單線程,它所有的事件都基於輪詢的方式來達到非同步處理。

但是,諸如node這樣單線程非同步為主的平台實際上都是需要驅動支持的。如果你用一個同步的資料庫驅動來做你node.js服務的支撐,那可就完全無法達到node標榜的高並發了。這也是為什麼Node的作者沒有選用lua做為node這個平台的實現語言,因為lua的同步庫太多,人們往往不自覺、不自知的就去用同步的lib了,顯然有違他的初衷。

而如果我們把Node這個整個平台作為一套解決方案來看,那麼它的前端(語言)雖然是單線程的,但後端則可能是多路復用的非阻塞I/O或者線程池模擬的非同步I/O。在這個方案中,語言前端可以基於EL的輪詢來查看句柄的狀態,而同時後端則可以利用epoll/kqueue/IOCP等技術來進行高性能的非同步I/O並且做為後端支撐。

當然除此以外還是有些別的高並發模型的,比如Actor什麼的.

@液漏醬

//相對來說,這個可能比較接近你想表達的意思,但實際上這段代碼不能說是準確的驗證,勉強算模擬吧。

var
fs=require("fs"),
EventEmitter=require("events").EventEmitter,
event=new EventEmitter(),
i=1

fs.open("Asyi.js", "r", function(err, fd){
setTimeout(function(){
console.log("lu");
},100) //讀的太快,delay一下
});

event.on("test",function(){
console.log("Current Number:"+(i++))
})

setInterval(function(){ //用0來模擬連續的串列事件。
event.emit("test")
},0)


單線程只有單任務,後續任務只是被放進隊列排隊。

同步和非同步,用Loupe直觀地理解Javascript運行時


node的單線程只是指他的業務代碼(handle),永遠由一個線程執行。如果典型的node web應用,背後還是由accept 線程,io線程,work線程(js),其他輔助線程組成一個線程池。

js線程維護一個task隊列,不斷執行task。所有任務都串列執行。

如果你想同時處理多個任務,只能通過子進程來處理。比如你的api有cpu密集行和io密集型兩種類型,為了避免cpu密集型阻塞IO密集task,你可以在主進程處理IO密集task,另外開闢子進程處理cpu密集性task。可以參考child_process Node.js v7.10.0 Documentation,node提供主進程和子進程高效的管道通訊。


首先,「但是對於NodeJs來說,因為他本身是單線程的」這句話就是不正確的,node.js從來沒有說過自己是單線程。你可以試試Google "single thread site:http://nodejs.org"

江湖傳言node.js是單線程,大概是指所有Javascript代碼是跑在一個線程里。但是其他node.js底層的、非Javascript的代碼,是跑在多線程環境上的。

謝謝 @Makoto Ruu提醒,下面代碼的例子輸出是錯誤的。體會其中奧妙就好了。。

==============================錯誤的例子==================================

舉個fs.open()的例子來說明這個問題,請看如下代碼:

fs.open("ipz900.avi", "r", function(err, fd){
console.log("lu");
});
while (1) {
console.log("Thread A");
}

在JS線程(線程A)執行到fs.open()的時候,會有另外一個線程(線程B)調用system call來打開這個文件。但此時你JS這個線程A繼續在跑,會走到while裡面列印N行Thread A。當線程B打開了文件之後,再來把callback裡面的代碼「插入」到線程A裡面。因此在線程A看起來,整個代碼執行是線性的。

根據線程B打開文件的速度,這段代碼實際輸出應該是:

ThreadA
ThreadA
ThreadA
...
...
...
ThreadA
lu
ThreadA
ThreadA
ThreadA

如果錯誤,請指正,謝謝!


我們經常提到Node是單線程,是指JavaScript在單線程中運行罷了,其實內部完成I/O任務的是線程池,而線程池是多線程的。

那麼Node是如何做到非同步I/O的呢?
主要靠: 事件循環,觀察者,請求對象和I/O線程池.

1. 每次事件循環時,觀察者被詢問是否有要處理的事件。如有,則把事件封裝到請求對象,並把請求對象送到線程池。

2. 待處理完成後,通過回調函數通知觀察者,然後將其當做事件處理?

Learn more on 朴靈的深入淺出Node.js第3章。(強烈推薦)


回復 液漏醬,你這個例子側面證明一個問題,nodejs官網裡也提到過的,nodejs適合用於io密集型的應用而非cpu密集型的應用,結果肯定不是如樓主所說,上面也有知友說了,「lu」永遠沒機會列印,姑且就把這個while死循環當成一個高cpu密集計算的段落吧(實際上,這個while循環沒有把cpu卡死而是把內存卡死了),列印lu這個callback被執行的機會是在下一輪事件循環的時候(也只是可能,有可能fs.open還沒有執行完畢了,那麼這個callback可能又得輪到下下輪甚至下下下輪.......事件循環里了,但是有一點肯定的是:這個callbak不會在本輪事件循環里被觸發)

那麼怎麼讓這個lu列印出來呢,可能有知友想到了nodejs里提供的process.nextTick函數,這裡我暫且不用死循環來構建這個問題,繼續作者的思路,我們改成:

var fs=require("fs");

fs.readFile("a.txt", "utf-8", function (err, contents) {

console.log("lu");

});

for(var i =1;i&<10000;i++){

process.nextTick(doThread(i));

}

function doThread(i){

return function(){

console.log("Thread A:"+i);

}

}

實測發現,lu在最後才列印:

......

Thread A:9997

Thread A:9998

Thread A:9999

lu

如果i無限大,那麼lu永遠沒機會列印了,可能知友還記得nodejs開發手冊說過nextTick適用場景之一就是,如果某個函數中包含了一個複雜計算,那麼可以改寫成:

function someMethod(){

console.log("simple logic code");

process.nextTick(doComlicatedCompute);

}

function doComlicatedCompute(){

console.log("do some complicated calculation in this method");

}

其實就相當於someMethod執行到console.log("simple logic code");之後,交出cpu,讓事件池裡的別的事件可以擁有cpu執行了,而doComlicatedCompute這個callback放到了本次事件輪詢的末端,等著觸發,所以上面的例子,如果i足夠大,那麼lu依然沒有被列印出來的機會,因為如上所說【列印lu這個callback被執行的機會是在下一輪事件循環的時候】,本次事件循環還沒結束呢!!

那麼上面代碼到底怎樣才能讓lu列印出來呢,nodejs設計者提供了這麼一個函數,setImmediate(your_callback),setImmediate是什麼呢,好吧,我用我自己的大白話描述吧,這個東東也是讓你的callback放到本次事件輪詢的末端,但是呢,有個好處,只執行一次,也就是說,可能上下文執行環境調用了多次setImmediate函數,把n個callback函數塞到了事件池的末端,但是呢對於【本次】事件輪詢來說,只執行第一個,下一次事件輪詢執行第二個,第n次事件輪詢才能執行第n個setImmediate放到事件池裡的回調,相比nextTick,setImmediate不會造成事件池飢餓,所以慎用nextTick啊,網上就有例子,遞歸調用函數里使用過多的nextTick,nodejs會報warning警告你:

nextTick塞了太多的callback到事件池裡了,下一輪事件輪詢里的callback可能沒機會被執行了,建議使用setImmediate......

【ps:nextTick維護了一個自己的callback數組,setImmediate維護了一個自己的callback鏈表】

好吧,扯遠了,現在我們改寫下函數如下:

var fs=require("fs");

fs.readFile("a.txt", "utf-8", function (err, contents) {

console.log("lu");

});

for(var i =1;i&<10000;i++){

setImmediate(doThread(i));

}

function doThread(i){

return function(){

console.log("Thread A:"+i);

}

}

本段函數執行完發生的事情是,fs.readFile後塞了一個列印lu的callback到事件池裡繼續往下執行,for循環里通過setImmediate塞了10000個callback到事件池裡,立即執行第一個setImmediate放的callback(console.log("Thread A:"+1);),好的,本輪事件輪詢結束,開始下一輪羅,此時事件池裡有些啥callback呢,第一個是列印lu的callback咯,第二個以後都是來自於setImmediate放的callback了

列印結果如下:

Thread A:1

lu

Thread A:2

Thread A:3

......

好吧,既然說到這裡那麼我就來個例子徹底讓大家弄明白setImmediate和nextTick區別吧,代碼如下:

function callNextTick(msg) {

process.nextTick((function (msg) {

return function () {

console.log(msg);

}

})(msg));

}

function callSetImmediate(msg) {

setImmediate((function (msg) {

return function () {

console.log(msg);

}

})(msg));

}

function callSetImmediateIncludeTick(msg) {

setImmediate((function (msg) {

return function () {

console.log(msg);

process.nextTick(function () {

console.log("first fire tick in " + msg);

console.log("second fire tick in " + msg);

callSetImmediateIncludeTick(msg);

});

}

})(msg));

}

callSetImmediateIncludeTick(1);

callSetImmediate(2);

callNextTick(3);

callNextTick(4);

callSetImmediateIncludeTick(5);

console.log(6);

列印結果是:
6
3
4
1
fire tick in 1
2
5
fire tick in 5

第一輪: 6 -&>3 -&>4 -&>1 -&>fire tick in 1 【end】

第二輪: 2 【end】

第三輪:5 -&> fire tick in 5 【end】

將callSetImmediateIncludeTick稍稍改造一下如下:

function callSetImmediateIncludeTick(msg) {

setImmediate((function (msg) {

return function () {

console.log(msg);

process.nextTick(function () {

console.log("first fire tick in " + msg);

});

process.nextTick(function () {

console.log("second fire tick in " + msg);

});

callSetImmediateIncludeTick(msg);

}

})(msg));

}

這是我們看到的列印如下了:

6

3

4

1

first fire tick in 1

second fire tick in 1

2

5

first fire tick in 5

second fire tick in 5

1

first fire tick in 1

second fire tick in 1

5

first fire tick in 5

second fire tick in 5

1

first fire tick in 1

second fire tick in 1

5

first fire tick in 5

second fire tick in 5

........[無限重複下去]

第一輪: 6 -&>3 -&>4 -&>1 -&>first fire tick in 1-&>second fire tick in 1 【end】

第二輪: 2 【end】

第三輪: 5 -&> first fire tick in 5 -&>second fire tick in 5【end】

第四輪: 1 -&> first fire tick in 1 -&>second fire tick in 1【end】

第五輪: 5 -&> first fire tick in 5 -&>second fire tick in 5【end】

第六輪: 1 -&> first fire tick in 1 -&>second fire tick in 1【end】

第七輪: 5 -&> first fire tick in 5 -&>second fire tick in 5【end】

........

好吧,最後做個小總結吧,在某一輪中,其他的各種插進來的會回調執行完後,nextTick引入的callback一定會在本輪全部執行完畢,才輪到setImmediate引入的callback,setImmediate的callback可能有很多,但是一輪只執行一個喲,更深的原因大家去百度吧,反正我們使用的人能知道到這一個層面就夠了嘿嘿,


他們說的其實都不夠大白話,其實V8在我們面前耍了小把戲。其實現在js目前所有非同步和回調(fs除外),都只是塞進隊列而已,而當執行完"所有"代碼,再死循環執行隊列裡面的代碼而已(裝逼別名:事件循環)。這就是下面代碼為什麼不會輸出1的原因。

```node

setTimeout(()=&>{console.log(1);},1);

while(true);

```


嚴格來說,nodejs並非是單線程的,只能說單進程,原因在於,O_nonblock對於傳統文件句柄來說是無效的,所以其實nodejs的文件操作都是多線程去執行,Linux和windows有所區別,但本質都是多線程。

至於如何分發任務,因為只有主線程處理邏輯,所以也比較簡單,比如你有一個讀取文件操作,fs.readFile(xxx,cb);這個時候就把cb放入回調隊列,等主線程邏輯跑完了,然後就去從頭到尾check回調隊列,發現已經完成的事件,就執行相應的回調。所有CB都check完成後,並且CB也執行完成後,又從頭開始check回調隊列,這就是事件循環。其實nodejs基本就兩個是非阻塞的(注意,這裡非阻塞和非同步是不同概念),一個是磁碟IO,一個是網路IO,磁碟IO大部分是多線程實現,網路IO是O_nonblock實現。


JS執行引擎就相對於一個任務隊列,多線程的底層提交結果給這個「任務隊列」,然後JS就按順序執行。


推薦閱讀:

操作系統用戶級線程能夠調用內核嗎?

TAG:計算機 | Nodejs | 多線程 | 進程 |