nodejs中使用多線程編程的方法實(shí)例
在以前的博文別說(shuō)不可能,nodejs中實(shí)現(xiàn)sleep中,我向大家介紹了nodejs addon的用法。今天的主題還是addon,繼續(xù)挖掘c/c++的能力,彌補(bǔ)nodejs的弱點(diǎn)。
我曾多次提到過(guò)nodejs的性能問(wèn)題。其實(shí)就語(yǔ)言本身而言,nodejs的性能還是很高的,雖然不及大多部靜態(tài)語(yǔ)言,但差距也并不大;相對(duì)其他動(dòng)態(tài)語(yǔ)言而言,速度優(yōu)勢(shì)非常明顯。但為什么我們常常說(shuō)nodejs不能勝任CPU密集型場(chǎng)景呢?因?yàn)橛捎谄鋯尉€程特性,對(duì)于CPU密集型場(chǎng)景,它并不能充分利用CPU。計(jì)算機(jī)科學(xué)中有一個(gè)著名的Amdahl定律:
假設(shè)總工作量W,可以分解為兩個(gè)部分:只能串行計(jì)算的Ws和允許并行計(jì)算的Wp。那么,在p個(gè)CPU并行計(jì)算的情況下,性能上能夠帶來(lái)speedup倍的提升。Amdahl定律描述了并行能做到的和不能做到的。它是一種理想情況,實(shí)際情況會(huì)復(fù)雜得多。比如并發(fā)很可能會(huì)引起資源的爭(zhēng)奪,需要增加各種鎖,從而常常讓并行處于等待狀態(tài);并發(fā)還會(huì)額外帶來(lái)操作系統(tǒng)對(duì)線程調(diào)度切換的時(shí)間開(kāi)銷(xiāo),增加Ws。不過(guò),當(dāng)一項(xiàng)任務(wù)中,Wp比Ws大得多,并且有多個(gè)CPU核心可供使用時(shí),并行帶來(lái)的性能提升是相當(dāng)可觀的。
好,回到nodejs上。我們?cè)O(shè)想一個(gè)計(jì)算場(chǎng)景:計(jì)算4000000內(nèi)的質(zhì)數(shù)數(shù)目。這個(gè)場(chǎng)景編程實(shí)現(xiàn)的時(shí)候,以除法運(yùn)算為主,不涉及內(nèi)存、對(duì)象等操作,理論上能夠確保讓nodejs以相對(duì)較快的速度運(yùn)行,不會(huì)落后c太多,便于對(duì)比。
javascript尋找質(zhì)數(shù)的方法已經(jīng)在這篇博客中提供了,直接抄過(guò)來(lái):
function zhishu_js(num) {
if (num == 1) {
return false;
}
if (num == 2) {
return true;
}
for (var i = 2; i <= Math.sqrt(num); i++) {
if (num % i == 0) {
return false;
}
}
return true;
}
再寫(xiě)一個(gè)c語(yǔ)言版本的:
#include <math.h>
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中,我們用一個(gè)從1到4000000的循環(huán)來(lái)檢索質(zhì)數(shù);c語(yǔ)言中,我們?cè)O(shè)置若干個(gè)線程,定義count為4000000,每個(gè)線程做如下操作要:如果count大于0,則取出count的值,并計(jì)算是否為質(zhì)數(shù),同時(shí)將count減1。根據(jù)這個(gè)思路,javascript版本的很容易寫(xiě):
var count = 0;
for (j = 1; j < 4000000; j++) {
if(zhishu(j)){
count++;
}
}
關(guān)鍵難點(diǎn)就是c語(yǔ)言的多線程編程。早期c/c++并沒(méi)有考慮并行計(jì)算的需求,所以標(biāo)準(zhǔn)庫(kù)中并沒(méi)有提供多線程支持。而不同的操作系統(tǒng)通常實(shí)現(xiàn)也是有區(qū)別的。為了避免這種麻煩,我們采用pthread來(lái)處理線程。
下載pthread最新版本。由于我對(duì)gyp不熟,link依賴(lài)lib搞了半天沒(méi)搞定,最后我的方式是,直接把pthread的源代碼放到了項(xiàng)目目錄下,并在binding.gyp中把pthread.c添加到源代碼列表中,在編譯項(xiàng)目的時(shí)候把pthread也編譯一次。修改后的binding.gyp是這樣的:
{
"targets": [
{
"target_name": "hello",
"sources": [ "hello.cc","pthreads/pthread.c" ],
"include_dirs": [
"<!(node -e \"require('nan')\")",
"pthreads"
],
"libraries": ["Ws2_32.lib"]
}
]
}
當(dāng)然了,我這種方法很麻煩,如果你們只添加pthread中l(wèi)ib和include目錄的引用,并且不出現(xiàn)依賴(lài)問(wèn)題,那是最好的,就沒(méi)有必要用我的方法來(lái)做。
那么接下來(lái)就進(jìn)入C/C++多線程的一切了,定義一個(gè)線程處理函數(shù):
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;
}
在線程與線程之間,對(duì)于count這個(gè)變量是相互競(jìng)爭(zhēng)的,我們需要確保同時(shí)只能有一個(gè)線程操作count變量。我們通過(guò) pthread_mutex_t lock; 添加一個(gè)互斥鎖。當(dāng)執(zhí)行 pthread_mutex_lock(&lock); 時(shí),線程檢查lock鎖的情況,如果已鎖定,則等待、重復(fù)檢查,阻塞后續(xù)代碼運(yùn)行;如果鎖已釋放,則鎖定,并執(zhí)行后續(xù)代碼。相應(yīng)的, pthread_mutex_unlock(&lock); 就是解除鎖狀態(tài)。
由于編譯器在編譯的同時(shí),進(jìn)行編譯優(yōu)化,如果一個(gè)語(yǔ)句沒(méi)有明確做什么事情,對(duì)其他語(yǔ)句的執(zhí)行也沒(méi)有影響時(shí),會(huì)被編譯器優(yōu)化掉。在上面的代碼中,我加入了統(tǒng)計(jì)質(zhì)數(shù)數(shù)量的代碼,如果不加的話,像這樣的代碼:
for (int j = 0; j < 4000000; j++) {
zhishu(j);
}
是會(huì)直接被編譯器跳過(guò)的,實(shí)際不會(huì)運(yùn)行。
添加addon的寫(xiě)法已經(jīng)介紹過(guò)了,我們實(shí)現(xiàn)從javascript接收一個(gè)參數(shù),表示線程數(shù),然后在c中創(chuàng)建指定數(shù)量的線程完成質(zhì)數(shù)檢索。完整代碼:
#include <nan.h>
#include <math.h>
#include <iostream>
#include "pthreads\pthread.h"
#define MAX_THREAD 100
using namespace v8;
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可以創(chuàng)建線程,默認(rèn)是joinable的,這個(gè)時(shí)候子線程受制于主線程;phread_join阻塞住主線程,等待子線程join,直到子線程退出。如果子線程已退出,則phread_join不會(huì)做任何事。所以對(duì)所有的線程都執(zhí)行thread_join,可以保證所有的線程退出后才會(huì)例主線程繼續(xù)進(jìn)行。
完善一下nodejs腳本:
var zhishu_c=require('./build/Release/hello.node').zhishu;
function zhishu(num) {
if (num == 1) {
return false;
}
if (num == 2) {
return true;
}
for (var i = 2; i <= Math.sqrt(num); i++) {
if (num % i == 0) {
return false;
}
}
return true;
}
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è)試結(jié)果:
單線程時(shí),雖然C/C++的運(yùn)行速度是nodejs的181%,但這個(gè)成績(jī)我們認(rèn)為在動(dòng)態(tài)語(yǔ)言中,還是非常不錯(cuò)的。雙線程時(shí)速度提升最明顯,那是因?yàn)槲业碾娔X是雙核四線程CPU,這個(gè)時(shí)候已經(jīng)可能在使用兩個(gè)核心在進(jìn)行處理。4線程時(shí)速度達(dá)到最大,此時(shí)應(yīng)該是雙核四線程能達(dá)到的極限,當(dāng)線程再增加時(shí),并不能再提升速度了。上述Amdahl定律中,p已達(dá)上限4。再增加線程,會(huì)增加操作系統(tǒng)進(jìn)程調(diào)度的時(shí)間,增加鎖的時(shí)間,盡管同時(shí)也能增加對(duì)CPU時(shí)間的競(jìng)爭(zhēng),但總體而言,Ws的增加更加明顯,性能是下降的。如果在一臺(tái)空閑的機(jī)器上做這個(gè)實(shí)驗(yàn),數(shù)據(jù)應(yīng)該會(huì)更好一點(diǎn)。
從這個(gè)實(shí)驗(yàn)中,我們可以得出這樣的結(jié)論,對(duì)于CPU密集型的運(yùn)算,交給靜態(tài)語(yǔ)言去做,效率會(huì)提高很多,如果計(jì)算中較多涉及內(nèi)存、字符串、數(shù)組、遞歸等操作(以后再驗(yàn)證),性能提升更為驚人。同時(shí),合理地利用多線程能有效地提高處理效率,但并不是線程越多越好,要根據(jù)機(jī)器的情況合理配置。
對(duì)于nodejs本身,的確是不擅長(zhǎng)處理CPU密集的任務(wù),但有了本文的經(jīng)驗(yàn),我想,想克服這個(gè)障礙,并非什么不可能的事情。
相關(guān)文章
node.js中的fs.truncate方法使用說(shuō)明
這篇文章主要介紹了node.js中的fs.truncate方法使用說(shuō)明,本文介紹了fs.truncate的方法說(shuō)明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12前端node Session和JWT鑒權(quán)登錄示例詳解
關(guān)于前端鑒權(quán)登錄是比較常見(jiàn)的需求了,本文將從服務(wù)端渲染和前后端分離的不同角度下演示鑒權(quán),為大家介紹前端node Session和JWT鑒權(quán)登錄示例詳解2022-07-07詳解Nodejs 通過(guò) fs.createWriteStream 保存文件
本篇文章主要介紹了Nodejs 通過(guò) fs.createWriteStream 保存文件,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10node腳本實(shí)現(xiàn)自動(dòng)化簽到和抽獎(jiǎng)功能
本文主要介紹了node腳本實(shí)現(xiàn)自動(dòng)化簽到和抽獎(jiǎng)功能,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01nodejs之koa2請(qǐng)求示例(GET,POST)
本篇文章主要介紹了nodejs之koa2請(qǐng)求示例(GET,POST),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-08-08使用socket.io實(shí)現(xiàn)簡(jiǎn)單聊天室案例
這篇文章主要介紹了使用socket.io實(shí)現(xiàn)簡(jiǎn)單聊天室案例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-01-01npm安裝報(bào)錯(cuò)npm ERR! Error: EPERM: operation&
這篇文章主要為大家介紹了npm安裝報(bào)錯(cuò)npm ERR! Error: EPERM: operation not permitted解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07nodejs報(bào)digital?envelope?routines::unsupported錯(cuò)誤的最新解決方法
這篇文章主要介紹了nodejs報(bào)digital?envelope?routines::unsupported錯(cuò)誤的最新解決方法,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-02-02