Nodejs多线程,真正的非阻塞
Nodejs多线程,真正的非阻塞
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多线程支持
Nodejs多线程,真正的非阻塞
背景介绍
Node.js 自推出以来,因其单线程模型而备受关注。这种模型通过异步I/O操作来提高性能,但在处理 CPU 密集型任务时却显得力不从心。虽然 Node.js 的异步机制可以将某些耗时操作放入事件循环中,但单线程的限制仍会导致阻塞。
协同程序(Fibers)与线程(Threads)
在讨论如何解决这个问题之前,我们需要了解两个重要概念:
-
Fibers:又称纤程,是一种用户级的轻量级线程。它们不能并行执行,只能按顺序运行,但可以避免线程切换的开销。
-
Threads:又称线程,可以在同一时刻并行执行,但需要管理线程间的同步和资源竞争问题。
解决方案:threads_a_gogo
为了克服 Node.js 在处理 CPU 密集型任务时的瓶颈,我们可以使用 threads_a_gogo
模块。该模块允许你在 Node.js 中创建真正的多线程环境,从而有效利用多核处理器的能力。
示例代码
以下是一个使用 threads_a_gogo
模块来计算斐波那契数列的例子:
// 安装 threads_a_gogo 模块
// npm install threads_a_gogo
const threads = require('threads_a_gogo');
function fibo(n) {
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
console.time('8 thread');
const numThreads = 8; // 创建线程池,最大数为8
const threadPool = threads.createPool(numThreads).all.eval(fibo); // 为线程池注册程序
const i = 8;
const cb = function(err, data) { // 注册线程执行完毕的回调函数
console.log(data);
if (--i === 0) {
threadPool.destroy();
console.timeEnd('8 thread');
}
};
// 开始向线程池中执行fibo(40)这个任务
for (let j = 0; j < 8; j++) {
threadPool.any.eval('fibo(40)', cb);
}
结果对比
为了展示 threads_a_gogo
模块的效果,我们还做了几个对比实验:
- 未使用多线程:异步方式执行 8 次
fibo(40)
计算,耗时约 23346ms。 - 使用
threads_a_gogo
:多线程方式执行 8 次fibo(40)
计算,耗时约 9510ms。 - 使用
cluster
模块:多进程方式执行 8 次fibo(40)
计算,耗时约 11925ms。
可以看到,使用 threads_a_gogo
模块的多线程方式明显提升了计算速度,尤其是在多核处理器上表现尤为显著。
总结
threads_a_gogo
模块提供了一种简单且高效的方式来解决 Node.js 在处理 CPU 密集型任务时的瓶颈问题。它不仅提高了性能,还保持了良好的可维护性和易用性。如果你正在寻找一种方法来增强 Node.js 在处理高负载任务时的表现,那么 threads_a_gogo
是一个值得考虑的选择。
最后文章的连接无法打开。。
修改了,不过貌似被墙了。。。爱、唉
threads_a_gogo貌似不支持windows哦 回去用linux试试看
居然用 eval
, 会不会蛋疼的… 文章好棒.
现在有没有其他多线程实现出来呢?
此 eval 可能非彼 eval 也
貌似最近更新在1年前了诶…
Fibers and Threads in node.js – what for? 访问不了?看看这个截图
http://nfs.nodeblog.org/9/d/9d4da654f1eca2f24b3215940b9e1b45.png
我单位翻墙是福利,所以能看到,回到家里也要翻墙了才能看。 这几天在看这个TAGG模块的源码,打算强化他一下,它现在这个模块只能字符串传递,最好能直接传对象的引用,不然效率太低了。
不着调jorge什么来头,官方没反应。。。。
代码使用的pthread标准库,貌似windows下没法使用
不知道是否有源码,自己在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服务器,用进程要好过线程。
不错,学习
还是协程吧~
mark
马克
在Node.js中,处理CPU密集型任务时单线程模型确实会遇到性能瓶颈。为了解决这个问题,我们可以利用多线程模块如threads_a_gogo
来实现真正的多线程支持。
示例代码
首先安装threads_a_gogo
模块:
npm install threads_a_gogo
然后使用以下代码来实现多线程计算:
const threadPool = require('threads_a_gogo').createPool(8); // 创建8个线程的线程池
function fibo(n) {
return n > 1 ? fibo(n - 1) + fibo(n - 2) : 1;
}
console.time('8 threads');
let i = 8;
const cb = (err, data) => {
if (err) throw err;
console.log(data);
if (--i === 0) {
threadPool.destroy();
console.timeEnd('8 threads');
}
};
for (let j = 0; j < 8; j++) {
threadPool.any.eval('fibo(40)', cb);
}
解释
- 创建线程池:
threadPool.createPool(8)
创建了一个包含8个线程的线程池。 - 定义计算函数:
fibo(n)
是一个递归计算斐波那契数列的函数。 - 启动计时器:
console.time('8 threads')
开始记录运行时间。 - 定义回调函数:
cb
是每个线程执行完成后调用的回调函数。 - 执行任务:
threadPool.any.eval('fibo(40)', cb)
将fibo(40)
任务分发到线程池中的任意一个线程执行,并在完成后调用回调函数。 - 结束计时器:所有任务完成后,销毁线程池并结束计时器。
通过这种方式,我们可以有效地利用多核CPU的优势,提高CPU密集型任务的执行效率。