在單線程的情況下,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. 待處理完成後,通過回調函數通知觀察者,然後將其當做事件處理?
回復 液漏醬,你這個例子側面證明一個問題,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:9998Thread A:9999lu如果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:1lu Thread A:2Thread 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的原因。```nodesetTimeout(()=&>{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就按順序執行。
推薦閱讀: