Open zhaobinglong opened 3 years ago
/**
* 事件循环主体,主线程择机执行
* 循环遍历事件队列
* 处理非IO任务
* 处理IO任务
* 执行回调,返回给上层
*/
eventLoop:function(){
// 如果队列不为空,就继续循环
while(this.globalEventQueue.length > 0){
// 从队列的头部拿出一个事件
varevent= this.globalEventQueue.shift();
// 如果是耗时任务
if(isIOTask(event)){
// 从线程池里拿出一个线程
varthread = getThreadFromThreadPool();
// 交给线程处理
thread.handleIOTask(event)
}else{
// 非耗时任务处理后,直接返回结果
varresult = handleEvent(event);
// 最终通过回调函数返回给V8,再由V8返回给应用程序
event.callback.call(null,result);
}
}
}
线程池接到任务以后,直接处理IO操作,比如读取数据库,当 I/O 任务完成以后就执行回调,把请求结果存入事件中,并将该事件重新放入队列中,等待循环,最后释放当前线程,当主线程再次循环到该事件时,就直接处理了。
/**
* 处理IO任务
* 完成后将事件添加到队列尾部
* 释放线程
*/
handleIOTask:function(event){
//当前线程
varcurThread = this;
// 操作数据库
varoptDatabase = function(params,callback){
varresult = readDataFromDb(params);
callback.call(null,result)
};
// 执行IO任务
optDatabase(event.params,function(result){
// 返回结果存入事件对象中
event.result = result;
// IO完成后,将不再是耗时任务
event.isIOTask = false;
// 将该事件重新添加到队列的尾部
this.globalEventQueue.push(event);
// 释放当前线程
releaseThread(curThread)
})
}
所有的事件入队的队列应该是一个数据结构,按顺序处理事件循环,直到队列为空。但是在 Node 中是如何发生的,完全不同于反应器模式描述的那样。因此有哪些差异?
在 NodeJS 中不止一个队列,不同类型的事件在它们自己的队列中入队。在处理完一个阶段后,移向下一个阶段之前,事件循环将会处理两个中间队列,直到两个中间队列为空。那么这里有多少个队列呢?中间队列是什么?
问:事件循环机制有多少个队列参与, 答:四个主要队列,两个中间队列
注意,尽管我在这里都简单说 “队列”,它们中的一些实际上是数据结构的不同类型(timers 被存储在最小堆里)除了四个主要的队列,这里另外有两个有意思的队列,我之前提到的 “中间队列”,被 Node 处理。尽管这些队列不是 libuv 的一部分,但是 NodeJS 的一部分。它们是:
因为事件循环的每个阶段(timers 队列,IO 事件队列,immediate 队列,close 处理器队列 ,四个主要的阶段),在移到下一个阶段之前,Node 会检查 nextTick 队列是否有入队的事件。如果队列不为空,Node 将会立马开始处理队列直到队列为空。
这引入了一个新的问题。递归地或者重复地使用 process.nextTick 往 nextTick 队列添加事件,可能会导致 I/O 或者其他的队列永远饿死(执行不到)。我们可以用以下代码片段模拟这个情节。
原理:node在每次执行完事件队列中的一个callback,就要检查一下nextTick 队列,如果nextTick 队列一直有事件,它就没空去执行下一次的事件循环了
尽管 immediates 队列与 timeout 的表现上有些许相似,他有自己独特的特点。
setImmediate(() => {
console.log('Hi, this is an immediate');
});
timer的过期时间即使将过期时间设置为 0 ,也不能保证得到马上执行。immediate 队列能够保证在事件循环的 I/O 阶段之后马上执行。
nextTickQueue 中的存储着被 process.nextTick() 触发的回调。microTaskQueue 保留着被 Promise 触发的回调。它们都不是事件循环的一部分(不是在 libUV 中开发的),而是在 node.js 中。在 C/C++ 和 Javascript 有交叉的时候,它们都是尽可能快地被调用。因此它们应该在当前操作运行后(不一定是当前 js 回调执行完)。 nextTick队列的优先级最高,事件循环每次处理完一个回调,都要进入nextTick队列清空一次任务。微任务队列的优先级低于nextTick队列,任务循环每次处理完一个队列的回调后,就要再次进入微任务队列
setImmediate(() => console.log('this is set immediate 1'));
setImmediate(() => console.log('this is set immediate 2'));
setImmediate(() => console.log('this is set immediate 3'));
setTimeout(() => console.log('this is set timeout 1'), 0);
setTimeout(() => {
console.log('this is set timeout 2');
process.nextTick(() => console.log('this is process.nextTick added inside setTimeout'));
}, 0);
setTimeout(() => console.log('this is set timeout 3'), 0);
setTimeout(() => console.log('this is set timeout 4'), 0);
setTimeout(() => console.log('this is set timeout 5'), 0);
process.nextTick(() => console.log('this is process.nextTick 1'));
process.nextTick(() => {
process.nextTick(console.log.bind(console, 'this is the inner next tick inside next tick'));
});
process.nextTick(() => console.log('this is process.nextTick 2'));
process.nextTick(() => console.log('this is process.nextTick 3'));
process.nextTick(() => console.log('this is process.nextTick 4'));
// 输出
this is process.nextTick 1
this is process.nextTick 2
this is process.nextTick 3
this is process.nextTick 4
this is the inner next tick inside next tick
this is set timeout 1
this is set timeout 2
this is process.nextTick added inside setTimeout // 事件循环中每个执行完一个回调,先检查nextTickQueue,如果有就执行
this is set timeout 3
this is set timeout 4
this is set timeout 5
this is set immediate 1
this is set immediate 2
this is set immediate 3
背景
node设计采用了单线程机制,但还可以承载高并发请求是因为node的单线程仅针对主线程来说,即每个node进程只有一个主线程来执行程序代码。node采用了事件驱动机制,将耗时阻塞的I/O操作交给线程池中的某个线程去完成,主线程只负责调度。
高并发策略
一般来说,高并发的解决方案就是提供多线程模型,服务器为每个客户端请求分配一个线程,使用同步 I/O,系统通过线程切换来弥补同步 I/O 调用的时间开销。比如 Apache 就是这种策略,由于 I/O 一般都是耗时操作,因此这种策略很难实现高性能,但非常简单,可以实现复杂的交互逻辑。
而事实上,大多数网站的服务器端都不会做太多的计算,它们接收到请求以后,把请求交给其它服务来处理(比如读取数据库),然后等着结果返回,最后再把结果发给客户端。因此,Node.js 针对这一事实采用了单线程模型来处理,它不会为每个接入请求分配一个线程,而是用一个主线程处理所有的请求,然后对 I/O 操作进行异步处理,避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。
nodejs线程池
说道线程池,在java领域中,jdk本身就提供了多种线程池实现,几乎所有的线程池都遵循以下模型(任务队列+线程池):
参考
https://zhuanlan.zhihu.com/p/37563244