精华 nodejs多线程,真正的非阻塞
发布于 3 年前 作者 DoubleSpout 45814 次浏览

node从他推出至今,充满赞美和饱受诟病的都是其单线程模型,所有的任务都在一个线程中完成(I/O等例外),优势的地方自然是免去了频繁切换线程的开销,以及减少资源互抢的问题等等,但是当nodejs面对cpu密集型模型的时候就力不从心了。尽管node拥有异步机制,可以把一些耗时算法丢入eventloop等待下个事件循环再做,但是因为其任然是单线程模型,所以终究会造成阻塞。

先解释一下两个名词,Fibers 和 Threads。 Fibers 又称纤程,可以理解为协同程序,类似py和lua都有这样的模型。使用Fibers可以避免对资源的互抢,减少cpu和内存的消耗,但是Fibers并不能够真正的并行执行,同一时刻只有一个Fibers在执行,如果在其中一个Fibers中执行过多的cpu操作或者写了个死循环,则整个主程序将卡死住。node中的异步事件循环模型就有点象这个。

Threads 又称线程,他可以在同一时刻并行的执行,他们共享主进程的内存,在其中某一时刻某一个threads锁死了,是不会影响主线程以及其他线程的执行。但是为了实现这个模型,我们不得不消耗更多的内存和cpu为线程切换的开销,同时也存在可能多个线程对同一内存单元进行读写而造成程序崩溃的问题。

很多让node支持多线程的方法是使用c/c++的addon来实现,在需要进行cpu密集型计算的地方,把js代码改写成c/c++代码,但是如果开发人员对c++不是很熟悉,一来开发效率会降低不少,二来也容易出bug,而且我们知道在addon中的c++代码除了编译出错外,是很难调试的,毕竟没有vs调试c++代码方便。

令人振奋的消息,我们为什么不让node也支持多线程模型呢?于是Jorge为我们开发出了一个让node支持多线程模型的模块:threads_a_gogo github地址:https://github.com/xk/node-threads-a-gogo

有了threads-a-gogo(以下简称TAGG)这个模块之后,我们可以让node做更多的事情,我记得以前我看过一篇文章,说node只能应付i/o密集型场景,在cpu密集型场景将完败给apache,因为apache是为每一个请求起一条线程的,所以在处理cpu密集型任务时一个线程的高强度计算不会很大程度的影响其他线程,类似的还有php的fastcgi,这也是很多拿node和php进行比较时,php的拥护者们一直提出的理论。

我们先来做一个简单的测试,用我们suqian大大最喜欢的斐波那契数组来看一下,加入了多线程的node有多么的强悍:(测试机器为4CPU) 没有使用TAGG的正常情况,异步也帮不了我们应对cpu密集型任务

    function fibo (n) {
      return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
    }
    var n=8
    function back(){
    	if(!--n) return console.timeEnd('no thread');
    }
    console.time('no thread');
    
    process.nextTick(function(){
    	console.log(fibo (40));
    	back();
    })
    process.nextTick(function(){
    	console.log(fibo (40));
    	back();
    })
    process.nextTick(function(){
    	console.log(fibo (40));
    	back();
    })
    process.nextTick(function(){
    	console.log(fibo (40));
    	back();
    })
    
    process.nextTick(function(){
    	console.log(fibo (40));
    	back();
    })

process.nextTick(function(){
	console.log(fibo (40));
	back();
})
process.nextTick(function(){
	console.log(fibo (40));
	back();
})
process.nextTick(function(){
	console.log(fibo (40));
	back();
})

我们模拟了8个异步的行为,测试用的node v0.8.16版本,所以 process.nextTick还是异步方法。最后我们输出结果为:

165580141
165580141
165580141
165580141
165580141
165580141
165580141
165580141
no thread: 23346ms

接下来我们使用TAGG模块来测试同样的执行8次斐波那契数组计算,看看成绩如何?

function fibo (n) {
  return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
console.time('8 thread');
var numThreads= 8; //创建线程池,最大数为8
var threadPool= require('threads_a_gogo').createPool(numThreads).all.eval(fibo); //为线程池注册程序
var i=8;
var cb = function(err,data){ //注册线程执行完毕的回调函数
	console.log(data);
	if(!--i){
		threadPool.destroy();
		console.timeEnd('8 thread');
	}
}
threadPool.any.eval('fibo(40)', cb); //开始向线程池中执行fibo(40)这个任务

threadPool.any.eval('fibo(40)', cb);

threadPool.any.eval('fibo(40)', cb);

threadPool.any.eval('fibo(40)', cb);

threadPool.any.eval('fibo(40)', cb);

threadPool.any.eval('fibo(40)', cb);

threadPool.any.eval('fibo(40)', cb);

threadPool.any.eval('fibo(40)', cb);

最重的结果:

165580141
165580141
165580141
165580141
165580141
165580141
165580141
165580141
8 thread: 9510ms

相比不使用多线程模型的node,使用了TAGG模块之后,我们在4CPU服务器上的测试结果要快上一倍还不止。 到这里我们看上去找到了一个比较完美的解决方案应对CPU密集型任务,但是可能有同学会说,我可以使用cluster来做相同的事情,下面我们来做一个使用cluster计算这些任务的情况:

var cluster = require('cluster');
var numCPUs = 8;
function fibo (n) {
  return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
console.time('8 cluster');
if (cluster.isMaster) {
  // Fork workers.
  for (var i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
  var i = 8;
  cluster.on('exit', function(worker, code, signal) {
		if(!--i){
			console.timeEnd('8 cluster');
			process.exit(0);
		}
  });
} else {
	console.log(fibo (40));
	process.exit(0);
}

代码上的复杂程度比使用TAGG要高的多,而且如果是动态计算斐波那契数组的结果,编码将更加困难,需要在fork时挂上不同的参数,出错的几率也更大。同时还有更重要的一个事情,如果是创建一个http服务器,如果4个cluster都在计算fibo,那第5个请求node将无法处理,而是用TAGG则还是能够正常处理的,所以cluster并不能解决单线程模型的cpu密集计算带来的阻塞问题,我们看下测试结果:

165580141
165580141
165580141
165580141
165580141
165580141
165580141
165580141
8 cluster: 11925ms

TAGG模块还有其他更多的功能,比如事件触发,平滑退出,查看线程工作状态等等,总之TAGG模块给node注入了新的活力,让node一直饱受诟病的处理cpu密集任务问题得到了一个妥善的解决,就算你不擅长c++代码,也能够轻松编写出多线程的真正的非阻塞node程序了。

最后分享一篇干货文章,相当很精彩的一篇博客: Fibers and Threads in node.js – what for?

tagg2,nodejs多线程模块,更好的api,支持nodejs原生模块,跨平台支持,windows,linux和mac 跨平台模块tagg2,让node多线程支持

36 回复

干货分析,感谢。收藏了

最后文章的连接无法打开。。

修改了,不过貌似被墙了。。。爱、唉

threads_a_gogo貌似不支持windows哦 回去用linux试试看

居然用 eval, 会不会蛋疼的… 文章好棒. 现在有没有其他多线程实现出来呢?

此 eval 可能非彼 eval 也

真真是极好的…

node官方是什么反应呢 Jorge又是什么来头

貌似最近更新在1年前了诶…

@seasonx4 既然传字符串了… 我想到除了 eval 只有存成文件调用的…

Fibers and Threads in node.js – what for? 访问不了?看看这个截图

http://nfs.nodeblog.org/9/d/9d4da654f1eca2f24b3215940b9e1b45.png

我单位翻墙是福利,所以能看到,回到家里也要翻墙了才能看。 这几天在看这个TAGG模块的源码,打算强化他一下,它现在这个模块只能字符串传递,最好能直接传对象的引用,不然效率太低了。

不着调jorge什么来头,官方没反应。。。。

@jiyinyiyong 这个eval和js的eval没关系,只是表示执行的意思

代码使用的pthread标准库,貌似windows下没法使用

@snoopy 支持啊!强烈关注下一篇文章!

不知道是否有源码,自己在windos下编译下应该也能用吧

好久了, 这项目

其实我比较喜欢看Bruno和Issac在博客里对骂, 哈哈。

http://bjouhier.wordpress.com/2012/04/14/node-js-awesome-runtime-and-new-age-javascript-gospel/

对骂的在后面的评论里。 摘一段:

Blockquote The node core team is not under any illusions that our current APIs are perfect. Are you insane? Handling errors is nightmarishly error prone! Why do you think that we’re doing this whole domains feature for v0.8? It’s just that most of us think that Streamline is is the wrong solution to the wrong part of the problem, that’s all. You don’t get to call people dogmatic just because they don’t agree with you. That’s childish and rude, Bruno.

比较赞同Bruno关于Asynchronous !== Callbacks(异步!=回调)的看法。至今我们实现异步的方法都只能通过回调,在业务逻辑比较复杂的情况下,很容易就掉入回调嵌套回调的回调地狱里面,如果能提供yield/generator类似的功能,无疑会大大简化程序流程和可读性。 另一方面,Issac坚持nodejs语法规范与v8同步,声称

Node is not a language design project

同样无可挑剔。看来只能寄望v8在将来加入yield/generator。

比较回调与yield实现异步的方式 ====callback====

var fs=require('fs');
fs.readFile(__filename,function(err,data){
    fs.writeFile(__filename+'.bak',data,function(err){
        console.log('file backup success!');
    })
});

=====yield=====

//npm install fibers before run this code
var Fiber=require('fibers');
var fs = require('fs');
Fiber(function(){
    var fiber=Fiber.current;
    fs.readFile(__filename,function(err,data){
        fiber.run(data);
    });
    var data=Fiber.yield();
    fs.writeFile(__filename+'.bak',data,function(err){
        console.log('file backup success!');
    });
}).run();

threads-a-gogo是否支持访问node原生模块? 例如在fibo函数内增加一行 if(n==40)var fs=require('fs'); fibo函数报错: ReferenceError: require is not defined 莫非tagg上下文(context)是纯粹的V8?这么强大。。。

目前TAGG不支持,今天通读了这个模块的源码,已经想好怎么改造了,要然用户在线程里获取一些ndoe的东西,不然局限性太大了,估计作者对js不是很熟悉,但是写C真的是有一手。

圈圈你也来了?外国人都很亢奋么,呵呵~

什么时候能够在线程函数中返回node模块??

node.js多线程就不少node.js了

如楼下所说,nodejs多线程就不叫nodejs了,我目前做不到在多线程中植入node模块的东西,想要让nodejs支持多线程参考我的另一篇文章 利用libuv编写异步多线程的addon实例 还有ifile这个模块就是一个nodejs利用libuv多线程的demo 高性能,跨平台,轻量级nodejs静态文件ifile模块

是的,js从诞生就注定是单线程的,无法改变的

一个噩梦的开始……

这个库的例子太有局限性,如果调用的func是异步的,没等执行pool就回收了。

pm2 难道。。。

线程,微软会比较喜欢。 太容易泄漏,太容易出现不可控单元。 另外,副作用也不利于多核。 在linux服务器,用进程要好过线程。

还是协程吧~

@DoubleSpout 你这家伙我似曾相识

回到顶部