在以前的博文別說不可能,nodejs中實現sleep中,我向大家介紹了nodejs addon的用法。今天的主題還是addon,繼續挖掘c/c++的能力,彌補nodejs的弱點。
我曾多次提到過nodejs的性能問題。其實就語言本身而言,nodejs的性能還是很高的,雖然不及大多部靜態語言,但差距也并不大;相對其他動態語言而言,速度優勢非常明顯。但為什么我們常常說nodejs不能勝任CPU密集型場景呢?因為由于其單線程特性,對于CPU密集型場景,它并不能充分利用CPU。計算機科學中有一個著名的Amdahl定律:
假設總工作量W,可以分解為兩個部分:只能串行計算的Ws和允許并行計算的Wp。那么,在p個CPU并行計算的情況下,性能上能夠帶來speedup倍的提升。Amdahl定律描述了并行能做到的和不能做到的。它是一種理想情況,實際情況會復雜得多。比如并發很可能會引起資源的爭奪,需要增加各種鎖,從而常常讓并行處于等待狀態;并發還會額外帶來操作系統對線程調度切換的時間開銷,增加Ws。不過,當一項任務中,Wp比Ws大得多,并且有多個CPU核心可供使用時,并行帶來的性能提升是相當可觀的。
好,回到nodejs上。我們設想一個計算場景:計算4000000內的質數數目。這個場景編程實現的時候,以除法運算為主,不涉及內存、對象等操作,理論上能夠確保讓nodejs以相對較快的速度運行,不會落后c太多,便于對比。
javascript尋找質數的方法已經在這篇博客中提供了,直接抄過來:
再寫一個c語言版本的:
bool zhishu(int num){
if (num == 1) {
return false;
}
if (num == 2) {
return true;
}
for (int i = 2; i <= sqrt(num); i++) {
if (num % i == 0) {
return false;
}
}
return true;
};
在nodejs中,我們用一個從1到4000000的循環來檢索質數;c語言中,我們設置若干個線程,定義count為4000000,每個線程做如下操作要:如果count大于0,則取出count的值,并計算是否為質數,同時將count減1。根據這個思路,javascript版本的很容易寫:
for (j = 1; j < 4000000; j++) {
if(zhishu(j)){
count++;
}
}
關鍵難點就是c語言的多線程編程。早期c/c++并沒有考慮并行計算的需求,所以標準庫中并沒有提供多線程支持。而不同的操作系統通常實現也是有區別的。為了避免這種麻煩,我們采用pthread來處理線程。
下載pthread最新版本。由于我對gyp不熟,link依賴lib搞了半天沒搞定,最后我的方式是,直接把pthread的源代碼放到了項目目錄下,并在binding.gyp中把pthread.c添加到源代碼列表中,在編譯項目的時候把pthread也編譯一次。修改后的binding.gyp是這樣的:
當然了,我這種方法很麻煩,如果你們只添加pthread中lib和include目錄的引用,并且不出現依賴問題,那是最好的,就沒有必要用我的方法來做。
那么接下來就進入C/C++多線程的一切了,定義一個線程處理函數:
void *thread_p(void *null){
int num, x=0;
do{
pthread_mutex_lock(&lock);
num=count--;
pthread_mutex_unlock(&lock);
if(num>0){
if(zhishu(num))x++;
}else{
break;
}
}while(true);
std::cout<<' '<<x<<' ';
pthread_exit(NULL);
return null;
}
在線程與線程之間,對于count這個變量是相互競爭的,我們需要確保同時只能有一個線程操作count變量。我們通過 pthread_mutex_t lock; 添加一個互斥鎖。當執行 pthread_mutex_lock(&lock); 時,線程檢查lock鎖的情況,如果已鎖定,則等待、重復檢查,阻塞后續代碼運行;如果鎖已釋放,則鎖定,并執行后續代碼。相應的, pthread_mutex_unlock(&lock); 就是解除鎖狀態。
由于編譯器在編譯的同時,進行編譯優化,如果一個語句沒有明確做什么事情,對其他語句的執行也沒有影響時,會被編譯器優化掉。在上面的代碼中,我加入了統計質數數量的代碼,如果不加的話,像這樣的代碼:
是會直接被編譯器跳過的,實際不會運行。
添加addon的寫法已經介紹過了,我們實現從javascript接收一個參數,表示線程數,然后在c中創建指定數量的線程完成質數檢索。完整代碼:
int count=4000000;
pthread_t tid[MAX_THREAD];
pthread_mutex_t lock;
void *thread_p(void *null){
int num, x=0;
do{
pthread_mutex_lock(&lock);
num=count--;
pthread_mutex_unlock(&lock);
if(num>0){
if(zhishu(num))x++;
}else{
break;
}
}while(true);
std::cout<<' '<<x<<' ';
pthread_exit(NULL);
return null;
}
NAN_METHOD(Zhishu){
NanScope();
pthread_mutex_init(&lock,NULL);
double arg0=args[0]->NumberValue();
int c=0;
for (int j = 0; j < arg0 && j<MAX_THREAD; j++) {
pthread_create(&tid[j],NULL,thread_p,NULL);
}
for (int j = 0; j < arg0 && j<MAX_THREAD; j++) {
pthread_join(tid[j],NULL);
}
NanReturnUndefined();
}
void Init(Handle<Object> exports){
exports->Set(NanSymbol("zhishu"), FunctionTemplate::New(Zhishu)->GetFunction());
}
NODE_MODULE(hello, Init);
phread_create可以創建線程,默認是joinable的,這個時候子線程受制于主線程;phread_join阻塞住主線程,等待子線程join,直到子線程退出。如果子線程已退出,則phread_join不會做任何事。所以對所有的線程都執行thread_join,可以保證所有的線程退出后才會例主線程繼續進行。
完善一下nodejs腳本:
console.time("c");
zhishu_c(100);
console.timeEnd("c");
console.time("js");
var count=0;
for (j = 1; j < 4000000; j++) {
if(zhishu(j)){
count++;
}
}
console.log(count);
console.timeEnd("js");
看一下測試結果:
單線程時,雖然C/C++的運行速度是nodejs的181%,但這個成績我們認為在動態語言中,還是非常不錯的。雙線程時速度提升最明顯,那是因為我的電腦是雙核四線程CPU,這個時候已經可能在使用兩個核心在進行處理。4線程時速度達到最大,此時應該是雙核四線程能達到的極限,當線程再增加時,并不能再提升速度了。上述Amdahl定律中,p已達上限4。再增加線程,會增加操作系統進程調度的時間,增加鎖的時間,盡管同時也能增加對CPU時間的競爭,但總體而言,Ws的增加更加明顯,性能是下降的。如果在一臺空閑的機器上做這個實驗,數據應該會更好一點。
從這個實驗中,我們可以得出這樣的結論,對于CPU密集型的運算,交給靜態語言去做,效率會提高很多,如果計算中較多涉及內存、字符串、數組、遞歸等操作(以后再驗證),性能提升更為驚人。同時,合理地利用多線程能有效地提高處理效率,但并不是線程越多越好,要根據機器的情況合理配置。
對于nodejs本身,的確是不擅長處理CPU密集的任務,但有了本文的經驗,我想,想克服這個障礙,并非什么不可能的事情。
新聞熱點
疑難解答