Nodejs的單線程與異步的初步理解

 

一、卡住我的代碼

卡住我的代碼是這樣的:

<code>var async = require('async');

var pushTask = function(name) {
    q.push(name, function(cb) {
        console.log('running: ' + name);
    }, function(err){
        console.log('finished: ' + name);
    });
}

var wait = function(mils) {
    var now = new Date;
    while(new Date - now &lt;= mils) ;
}

var q = async.queue(function(name, task, callback) {
    console.log('processing task: ' + name);
    task(callback);
}, 3);

pushTask('t1');
pushTask('t2');
pushTask('t3');
pushTask('t4');

wait(100);
console.log('waited 100ms');

pushTask('t5');
pushTask('t6');
pushTask('t7');
pushTask('t8');

wait(10000);
console.log('waited 1000ms');
</code>

簡單解釋一下。async的queue是一個任務隊列,在本例中設置了3個worker。我通過pushTask函數,向隊列中放入多個任務。期間等待了兩次,分別是100ms和1000ms。等待的目的是想讓放入的任務先執行,queue不是異步的嗎?

然而執行結果卻讓我意外:

<code>waited 100ms
waited 1000ms
processing task: t1
running: t1
processing task: t2
running: t2
processing task: t3
running: t3
processing task: t4
running: t4
processing task: t5
running: t5
processing task: t6
running: t6
processing task: t7
running: t7
processing task: t8
running: t8</code>

可以看到,提交的任務都沒有立即執行,而是讓我白等了1100ms。這是怎麼回事?我期望的輸出是這樣的:

<code>processing task: t1
running: t1
processing task: t2
running: t2
processing task: t3
running: t3
processing task: t4
running: t4
waited 100ms
processing task: t5
running: t5
processing task: t6
running: t6
processing task: t7
running: t7
processing task: t8
running: t8
waited 1000ms</code>

爲了解決這個問題,我與羣中的幾位朋友一起討論,最終基本弄明白了這個問題,非常感謝他們的幫助。

二、Nodejs是單線程嗎?

首先是這篇非常重要的文章:http://debuggable.com/posts/understanding-node-js:4bd98440-45e4-4a9a-8ef7-0f7ecbdd56cb

我們寫下的js代碼,是在單線程的環境中執行,但nodejs本身不是單線程的。如果我們在代碼中調用了nodejs提供的異步api(如IO等),它們可能是通過底層的c(c++?)模塊在另外的線程中完成。但對於我們自己的js代碼來說,它們處於單線程中。因爲異步函數執行完將結果通過回調函數傳給我們的時候,我們的代碼一次只能處理一個。

在這裏用debuggable.com上的那個文章中的一段比喻來講,非常容易理解。如下:

我們寫的js代碼就像是一個國王,而nodejs給國王提供了很多僕人。早上,一個僕人叫醒了國王,問他有什麼需要。國王給他一份清單,上面列舉了所有需要完成的任務,然後睡回籠覺去了。當國王回去睡覺之後,僕人才離開國王,拿着清單,給其它的僕人一個個佈置任務。僕人們各自忙各自的去了,直到完成了自己的任務後,纔回來把結果稟告給國王。國王一次只召見一個人,其它的人就在外面排着隊等着。國王處理完這個結果後,可能給他佈置一個新的任務,或者就直接讓他走了,然後再召見下一個人。等所有的結果都處理完了,國王就繼續睡覺去了。直接有新的僕人完成任務後過來找他。這就是國王的幸福生活。

這段話對於理解nodejs的運行方式非常重要。

在nodejs中,有一個隊列(先進先出),保存着一個個待執行的任務。第一個任務就是我們寫的js代碼,它最先被執行(相當於國王給第一個僕人任務清單)。在它執行完以後(國王睡回籠覺去了),其它的任務纔會加到隊列上(相當於第一個僕人按照清單給其它僕人分配任務)。

在我最上面的代碼中,我在提交任務時,兩次wait,實際上相當於國王在給第一個僕人清單時,突然發呆,僕人只能老老實實地等着,而不會去佈置任務。直到國王發了兩次呆之後,纔去睡覺(我們的代碼運行到結尾),這時僕人纔敢離開給其他人佈置任務。

這就是爲什麼會先出現兩個waited 1xxms,之後纔出現任務被執行的信息的原因。

三、process.nextTick

這篇文章也非常重要:http://howtonode.org/understanding-process-next-tick

nodejs的單線程讓羣中有些朋友很不滿,他們認爲如果我們需要進行一些密集計算(比如while(true)這樣的),豈不是把整個線程等卡死了?我在一些資料上看到,的確是有這個擔心,所以nodejs不適合用來開發cpu密集運算的程序,而適合做那些IO操作比較多,但本身不需要計算太多的程序。因爲IO操作通過都是通過異步由nodejs在其它線程中完成,所以不會影響到主線程。

但如果我們的程序中,難以避免地需要進行一些密集運算該怎麼辦?這時需要把計算分解爲可遞歸的步驟,計算一步後,使用process.nextTick將下一步放在隊列的最後,讓nodejs有機會去處理那些已經在等待的任務。

這裏舉一個例子,來自前面提到的howtonode上的文章:

<code>var http = require('http');

var wait = function(mils) {
    var now = new Date;
    while(new Date - now &lt;= mils);
};

function compute() {
    // performs complicated calculations continuously
    console.log('start computing');
    wait(1000);
    console.log('working for 1s, nexttick');
    process.nextTick(compute);
}

http.createServer(function(req, res) {
    console.log('new request');
     res.writeHead(200, {'Content-Type': 'text/plain'});
     res.end('Hello World');
}).listen(5000, '127.0.0.1');

compute();</code>

其中compute是一個密集計算的函數,我們把它變爲可遞歸的,每一步需要1秒(使用wait來代替密集運行)。執行完一次後,通過process.nextTick把下一次的執行放在隊列的尾部,轉而去處理已經處於等待中的客戶端請求。這樣就可以同時兼顧兩種任務,讓它們都有機會執行。

不過這種方式對於一個高訪問量的網站來說還是不夠,因爲每步需要1s,這個時間還是太長了。這種情況需要採用其它的方式處理(以我目前剛入門的能力來看還不知道如何解決)。

在羣中討論nextTick時,我們對它的處理產生了分歧。主要原因是由於文中的一句話:

In this model, instead of calling compute() recursively, we use process.nextTick() to delay the execution of compute() till the next tick of the event loop

有的同學認爲它是說“把某任務放在當前任務的下一個”,有的認爲是放在隊列的最尾,爭輪不休。最後老雷同志貼上了nodejs的源代碼,解決了這個問題:

 

從這幾行代碼中,我們可以看出很多信息:

  1. nextTick的確是把某任務放在隊列的最後(array.push)
  2. nodejs在執行任務時,會一次性把隊列中所有任務都拿出來,依次執行
  3. 如果全部順利完成,則刪除剛纔取出的所有任務,等待下一次執行
  4. 如果中途出錯,則刪除已經完成的任務和出錯的任務,等待下次執行
  5. 如果第一個就出錯,則throw error

看來有時候找半天資料不如看一眼源代碼。

四、注意事項

如前段所講,我們在js代碼中,一定要儘量避免如while(true)這樣的循環,或者密集計算。如果一定要這麼做,則應該想辦法把它分解爲可重要執行的小塊,通過process.nextTick將它分散開,讓所有的任務都有執行的機會。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章