zhaobinglong / myBlog

https://zhaobinglong.github.io/myBlog/
MIT License
7 stars 0 forks source link

nodejs之事件循环 #78

Open zhaobinglong opened 3 years ago

zhaobinglong commented 3 years ago

背景

node设计采用了单线程机制,但还可以承载高并发请求是因为node的单线程仅针对主线程来说,即每个node进程只有一个主线程来执行程序代码。node采用了事件驱动机制,将耗时阻塞的I/O操作交给线程池中的某个线程去完成,主线程只负责调度。

问:nodejs是单线程的语言吗?这句话不完全对,因为node只有一个主线程负责调度,还有一个线程池负责处理事件

高并发策略

一般来说,高并发的解决方案就是提供多线程模型,服务器为每个客户端请求分配一个线程,使用同步 I/O,系统通过线程切换来弥补同步 I/O 调用的时间开销。比如 Apache 就是这种策略,由于 I/O 一般都是耗时操作,因此这种策略很难实现高性能,但非常简单,可以实现复杂的交互逻辑。

而事实上,大多数网站的服务器端都不会做太多的计算,它们接收到请求以后,把请求交给其它服务来处理(比如读取数据库),然后等着结果返回,最后再把结果发给客户端。因此,Node.js 针对这一事实采用了单线程模型来处理,它不会为每个接入请求分配一个线程,而是用一个主线程处理所有的请求,然后对 I/O 操作进行异步处理,避开了创建、销毁线程以及在线程间切换所需的开销和复杂性。

nodejs线程池

说道线程池,在java领域中,jdk本身就提供了多种线程池实现,几乎所有的线程池都遵循以下模型(任务队列+线程池): image

参考

https://zhuanlan.zhihu.com/p/37563244

zhaobinglong commented 3 years ago

nodejs四层模型

事件循环六个阶段

zhaobinglong commented 3 years ago

nodejs事件模型

/**
 * 事件循环主体,主线程择机执行
 * 循环遍历事件队列
 * 处理非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);
        }
    }
}
zhaobinglong commented 3 years ago

IO线程模型

线程池接到任务以后,直接处理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)
    })
}
zhaobinglong commented 3 years ago

事件队列

所有的事件入队的队列应该是一个数据结构,按顺序处理事件循环,直到队列为空。但是在 Node 中是如何发生的,完全不同于反应器模式描述的那样。因此有哪些差异?

在 NodeJS 中不止一个队列,不同类型的事件在它们自己的队列中入队。在处理完一个阶段后,移向下一个阶段之前,事件循环将会处理两个中间队列,直到两个中间队列为空。那么这里有多少个队列呢?中间队列是什么?

问:事件循环机制有多少个队列参与, 答:四个主要队列,两个中间队列

四个主要队列

注意,尽管我在这里都简单说 “队列”,它们中的一些实际上是数据结构的不同类型(timers 被存储在最小堆里)除了四个主要的队列,这里另外有两个有意思的队列,我之前提到的 “中间队列”,被 Node 处理。尽管这些队列不是 libuv 的一部分,但是 NodeJS 的一部分。它们是:

image

zhaobinglong commented 3 years ago

IO 饿死

因为事件循环的每个阶段(timers 队列,IO 事件队列,immediate 队列,close 处理器队列 ,四个主要的阶段),在移到下一个阶段之前,Node 会检查 nextTick 队列是否有入队的事件。如果队列不为空,Node 将会立马开始处理队列直到队列为空。

这引入了一个新的问题。递归地或者重复地使用 process.nextTick 往 nextTick 队列添加事件,可能会导致 I/O 或者其他的队列永远饿死(执行不到)。我们可以用以下代码片段模拟这个情节。

原理:node在每次执行完事件队列中的一个callback,就要检查一下nextTick 队列,如果nextTick 队列一直有事件,它就没空去执行下一次的事件循环了

zhaobinglong commented 3 years ago

Immediates 队列

尽管 immediates 队列与 timeout 的表现上有些许相似,他有自己独特的特点。

setImmediate(() => {
   console.log('Hi, this is an immediate');
});

Immediates和timer的区别

timer的过期时间即使将过期时间设置为 0 ,也不能保证得到马上执行。immediate 队列能够保证在事件循环的 I/O 阶段之后马上执行。

nextTick队列 VS promise微任务队列

nextTickQueue 中的存储着被 process.nextTick() 触发的回调。microTaskQueue 保留着被 Promise 触发的回调。它们都不是事件循环的一部分(不是在 libUV 中开发的),而是在 node.js 中。在 C/C++ 和 Javascript 有交叉的时候,它们都是尽可能快地被调用。因此它们应该在当前操作运行后(不一定是当前 js 回调执行完)。 nextTick队列的优先级最高,事件循环每次处理完一个回调,都要进入nextTick队列清空一次任务。微任务队列的优先级低于nextTick队列,任务循环每次处理完一个队列的回调后,就要再次进入微任务队列

zhaobinglong commented 3 years ago

实例分析

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