天天看點

Node.js異步處理CPU密集型任務

 Node.js異步處理CPU密集型任務

Node.js擅長資料密集型實時(data-intensive real-time)互動的應用場景。然而資料密集型實時應用程式并不是隻有I/O密集型任務,當碰到CPU密集型任務時,比如要對資料加解密(node.bcrypt.js),資料壓縮和解壓(node-tar),或者要根據使用者的身份對圖檔做些個性化處理,在這些場景下,主線程緻力于做複雜的CPU計算,IO請求隊列中的任務就被阻塞。

Node.js主線程的event loop在處理所有的任務/事件時,都是沿着事件隊列順序執行的,是以在其中任何一個任務/事件本身沒有完成之前,其它的回調、監聽器、逾時、nextTick()的函數都得不到運作的機會,因為被阻塞的event loop根本沒機會處理它們,此時程式最好的情況是變慢,最糟的情況是停滞不動,像死掉一樣。           

一個可行的解決方案是新開程序,通過ipc通信,将CPU密集型任務交給子程序,子程序計算完畢後,再通過ipc消息通知主程序,并将結果傳回給主程序[1]。

和建立線程相比,開辟新程序的系統資源占用率大,程序間通信效率也不高。如果能不開新程序而是新開線程,将CPU耗時任務交給一個工作線程去做,然後主線程立即傳回,處理其他的IO請求,等到工作線程計算完畢後,通知主線程并将結果傳回給主線程。那麼在同時面對IO密集型和CPU密集型服務的場景下,Node.js的主線程也會變得輕松,并能時刻保持高相應度。

是以,和開程序相比,一個更加優秀的解決方案是:

1 不開程序,而是将CPU耗時操作交給程序内的一個工作線程完成。

2 CPU耗時操作的具體邏輯支援通過C++和Js實作。

3 js使用這個機制與使用IO庫類似,友善高效。

4 在新線程中運作一個獨立的V8 VM,與主線程的VM并發執行,并且這個線程必須  由我們自己托管。

為了實作以上四個目标,我們在Node中增加了一個backgroundthread線程,文章稍候會詳細解釋這個概念。在具體實作上,為Node增加了一個’pt_c’的内建C++子產品。這個子產品負責吧CPU耗時操作封裝成一個Task,抛給backgroundthread,然後立即傳回。具體的邏輯在另一個線程中處理,完成之後,設定結果,通知主線程。這個過程非常類似于異步IO請求。具體邏輯如下圖:

BackgroundThread

Node提供了一種機制可以将CPU耗時操作交給其他線程去做,等到執行完畢後設定結果通知主線程執行callback函數。以下是一段代碼,用來示範這個過程:

int main() {

    loop = uv_default_loop();

    int data[FIB_UNTIL];

    uv_work_t req[FIB_UNTIL];

    int i;

    for (i = 0; i < FIB_UNTIL; i++) {

        data[i] = i;

        req[i].data = (void *) &data[i];

        uv_queue_work(loop, &req[i], fib, after_fib);

    }

    return uv_run(loop, UV_RUN_DEFAULT);

}

其中函數uv_queue_work的定義如下:

UV_EXTERN int uv_queue_work(uv_loop_t* loop,

                            uv_work_t* req,

                            uv_work_cb work_cb,

                            uv_after_work_cb after_work_cb);

參數 work_cb 是在另外線程執行的函數指針,after_work_cb相當于給主線程執行的回調函數。

在windows平台上,uv_queue_work最終調用API函數QueueUserWorkItem來派發這個task,最終執行task 的線程是由作業系統托管的,每次可能都不一樣。這不滿足上述第四條。

因為我們要支援線上程中運作js代碼,這就需要開一個V8 VM,是以需要把這個線程固定下來,特定任務,隻交給這個線程處理。并且一旦建立,不管有沒有task,都不能随便退出。這就需要我們自己維護一個線程對象,并且提供接口,使得使用者可以友善的生成一個對象并且送出給這個線程的任務隊列。

在node程序啟動初始化過程中,加入一個建立background thread對象的過程。這個線程擁有一個taskloop,有任務就處理,沒有任務就等待在一個信号量上。多線程要考慮線程間同步的問題。線程同步隻發生在讀寫此線程的incomming queue 的時候。Node的主線程生成task後,送出到這個線程的incomming queue中,并激活信号量然後立即傳回。在下一次循環中,backgroundthread從incomming queue中取出所有的task,放入working queue,然後依次執行working queue中的task。主線程不通路working queue是以不需要加鎖。這樣做可以降低沖突。

這個線程在進入taskloop循環之前會建立一個獨立的v8 VM,專門用來執行backgroundjs的代碼。主線程的v8引擎和這個線程的可以并行執行。它的生命周期與Node程序的生命周期一緻。

BackgroundJs

可以把所有CPU耗時邏輯放入backgroundJs中,主線程通過生成一個task,指定好運作的函數和參數,抛給工作線程。工作線程在執行task的過程中調用在backgroundJs中的函數。BackgroundJs是一個.js檔案,在裡面添加CPU耗時函數。

background.js代碼示例:

var globalFunction = function(v){

var flag;

try

{

   flag = true;

   JSON.parse(v); 

catch(e)

   flag = false;

if(!flag)

   var err = ‘err‘;

   return err;

var obj = JSON.parse(v);

var a = obj.param1;

var b = obj.param2;

var i;

// simulate CPU intensive process...

for(i = 0; i < 95550000; ++i)

i += 100;

i -= 100;

return (a+b).toString();

運作node.js,在控制台輸入:

var bind  = process.binding(‘pt_c‘);

var obj = {param1: 123,param2: 456};

bind.jstask(‘globalFunction‘, JSON.stringify(obj), function(err, data){if(err) console.log("err"); else console.log(data);});

調用的方法是bind.jstask,稍後會解釋這個函數的用法。

以下是測試結果:

上面這個實驗操作步驟如下:

1 首先綁定’pt_c’内模組化塊

2 快速多次調用backgroundjs中的CPU耗時函數,上面的實驗中連續調用了三次。

當backgroundjs中的函數完成後,主線程接到通知,在新一輪的evenloop中,調用回調函數,列印出結果。這個實驗說明了CPU耗時操作異步執行。

方法jstask總共三個參數,前兩個參數為字元串,分别是background.js中的全局函數名稱,傳給函數的參數。最後一個參數是一個callback函數,異步留給主線程運作。

為什麼用字元串做參數?

為了适應各種不同的參數類型,就需要為C++函數提供各種不同的函數實作,這是非常受限制的。C++根據函數名擷取backgroundjs中的函數然後将參數傳遞給js。在js中,處理json字元串是非常容易的,是以采用字元串,簡化了C++的邏輯,js又能夠友善的生成和解析參數。同樣的理由,backgroundjs中函數的傳回值也為json串。

對C++的支援

在苛求性能的場景,’pt_c’允許加載一個.dll 檔案到node程序,這個dll檔案包含CPU耗時操作。js加載’pt_c’的時候,指定檔案名即可完成加載。

代碼示例:

bind.registermodule(‘node_pt_c.dll‘, ‘DllInit‘, ‘Json to Init‘);

bind.posttask(‘Func_example‘, ‘Json_Param‘, function(err, data){if(err) console.log("err"); else console.log(data);});

與backgroundjs相比,加載C++子產品多了一個步驟,這個步驟是調用bind.registermodule。這個函數負責将加載dll并負責對其初始化。一旦成功後,不能再加載其他子產品。所有的CPU耗時操作函數都應該在這個dll檔案中實作。

總結

這篇文章提出了backgroundjs這個新的概念,擴充了Node.js的能力,解決了Node在處理CPU密集任務時的短闆。這個解決方案使得使用Node的開發人員隻需要關注backgroundjs中的函數。比起多開程序或者新添加子產品的解決方案更高效,通用和一緻。

我們的代碼已經開源,您可以在 https://github.com/classfellow/node/tree/Ansy-CPU-intensive-work--in-one-process 

下載下傳。

支援backgroundjs一個穩定Node版本您可以在

    下載下傳。

參考文獻:

1 Node.js軟肋之CPU密集型任務

2  Why you should use Node.js for CPU-bound tasks,Neil Kandalgaonkar,2013.4.30;

3  http://nikhilm.github.io/uvbook/threads.html#inter-thread-communication

繼續閱讀