標籤:class http tar ext com get
原文來自於:http://www.infoq.com/cn/articles/new-idea-of-nodejs-asynchronous-processing-tasks?utm_source=infoq&utm_medium=popular_links_homepage
Node.js擅長資料密集型即時(data-intensive real-time)互動的應用情境。然而資料密集型即時應用程式並不是只有I/O密集型任務,當碰到CPU密集型任務時,比如要對資料加解密(node.bcrypt.js),資料壓縮和解壓(node-tar),或者要根據使用者的身份對圖片做些個人化處理,在這些情境下,主線程致力於做複雜的CPU計算,I/O請求隊列中的任務就被阻塞。
Node.js主線程的event loop在處理所有的任務/事件時,都是沿著事件隊列順序執行的,所以在其中任何一個任務/事件本身沒有完成之前,其它的回調、監聽器、逾時、nextTick()的函數都得不到啟動並執行機會,因為被阻塞的event loop根本沒機會處理它們,此時程式最好的情況是變慢,最糟的情況是停滯不動,像死掉一樣。
一個可行的解決方案是新開進程,通過IPC通訊,將CPU密集型任務交給子進程,子進程計算完畢後,再通過ipc訊息通知主進程,並將結果返回給主進程。
和建立線程相比,開闢新進程的系統資源佔用率大,處理序間通訊效率也不高。如果能不開新進程而是新開線程,將CPU耗時任務交給一個背景工作執行緒去做,然後主線程立即返回,處理其他的I/O請求,等到背景工作執行緒計算完畢後,通知主線程並將結果返回給主線程。那麼在同時面對I/O密集型和CPU密集型服務的情境下,Node的主線程也會變得輕鬆,並能時刻保持高響應度。
因此,和開進程相比,一個更加優秀的解決方案是:
- 不開進程,而是將CPU耗時操作交給進程內的一個背景工作執行緒完成。
- CPU耗時操作的具體邏輯支援通過C++和JS實現。
- JS使用這個機制與使用I/O庫類似,方便高效。
- 在新線程中運行一個獨立的V8 VM,與主線程的VM並發執行,並且這個線程必須由我們自己託管。
為了實現以上四個目標,我們在Node中增加了一個backgroundthread線程,文章稍候會詳細解釋這個概念。在具體實現上,為Node增加了一個pt_c的內建C++模組。這個模組負責把CPU耗時操作封裝成一個Task,拋給backgroundthread,然後立即返回。具體的邏輯在另一個線程中處理,完成之後,設定結果,通知主線程。這個過程非常類似於非同步I/O請求。具體邏輯如:
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,都不能隨便退出。這就需要我們自己維護一個線程對象,並且提供介面,使得使用者可以方便的產生一個對象並且提交給這個線程的任務隊列。
在綁定內建模組pt_c的時候,會建立一個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進程的生命週期一致。
// pt_c模組的初始化代碼void Init(Handle<Object> target, Handle<Value> unused, Handle<Context> context, void* priv) { //Create working thread, focus on cup intensive task if(!CWorkingThread::GetInstance().Start()){ return; } Environment* env = Environment::GetCurrent(context); // load dll, Including all the cpu-intensive functions NODE_SET_METHOD(target, "registermodule", RegisterModule); NODE_SET_METHOD(target, "posttask", PostTask); // post a task that run a cpu-intensive function defined in backgroundjs NODE_SET_METHOD(target, "jstask", JsTask);}
可以把所有CPU耗時邏輯放入backgroundJs中,主線程通過產生一個task,指定好啟動並執行函數和參數,拋給背景工作執行緒。背景工作執行緒在執行task的過程中調用在backgroundJs中的函數。BackgroundJs是一個.js檔案,在裡面添加CPU耗時函數。
background.js程式碼範例:
var globalFunction = function(v){ var obj; try { obj = JSON.parse(v); } catch(e) { return e; } 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,在控制台輸入:
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,稍後會解釋這個函數的用法。
以下是測試結果:
上面這個實驗操作步驟如下:
- 首先綁定pt_c內建模組。綁定的過程會調用模組初始化函數,在這個函數中,建立新線程。
- 快速多次調用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的時候,指定檔案名稱即可完成載入。
程式碼範例:
var bind = process.binding(‘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版本您可以在http://www.witch91.com/nodejs.rar下載。
參考文獻
- Node.js軟肋之CPU密集型任務
- Why you should use Node.js for CPU-bound tasks,Neil Kandalgaonkar,2013.4.30;
- http://nikhilm.github.io/uvbook/threads.html#inter-thread-communication
- 深入淺出Node.js 樸靈