node原理,事件循环,setTimeout/setImmediate/process.nextTick的差别

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010365819/article/details/84065485

事件循环

Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。

当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop),这是一个很笼统的描述,具体的细节东西,下面还有。其运行原理如下图所示:

 

这个图是整个 Node.js 的运行原理,从左到右,从上到下,Node.js 被分为了四层,分别是 应用层V8引擎层Node API层 和 LIBUV层。

应用层:   即 JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs

V8引擎层:  即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互

NodeAPI层:  为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互 。

LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心 。

 

无论是 Linux 平台还是 Windows 平台,Node.js 内部都是通过 线程池 来完成异步 I/O 操作的,而 LIBUV 针对不同平台的差异性实现了统一调用。因此,Node.js 的单线程仅仅是指 JavaScript 运行在单线程中(应用层是单线程的),而并非 Node.js 是单线程。

node对回调事件的处理完全是基于事件循环的tick的,因此具有几大特征:

1、在应用层面,JS是单线程的,业务代码中不能存在耗时过长的代码,否则可能会严重拖后续代码(包括回调)的处理。如果遇到需要复杂的业务计算时,应当想办法启用独立进程或交给其他服务进行处理。

2、回调是不精确,因为前面的原因,setTimeout并不能得到准确的超时回调。

3、不同类型的观察者,处理的优先级不同,idle观察者最先,I/O观察者其次,check观察者最后。

下面的这个图是一个完整的node运行流程:

 

 

关于观察者,从上图中可以很明显的看到,在整个事件循环过程中承担了最基本的数据结构的角色,所有的io请求或者网络请求都被封装成了观察者对象,事件循环通过观察者对象来调用回调函数。这里可以很明确的看到,观察者就是文件描述符表和callbach的和。这个图太直观,太明了了,事件循环的一切细节都在上面了。

说到这个观察者对象,有人会觉得难道这就是传说设计模式中的观察者模式吗?nonono,观察者模式是定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。而反观上面的事件循环机制,我们封装了一个又一个的观察者对象,然后事件循环通过观察者对象来获取异步操作结束之后返回的数据,并交给主线程来处理。

仔细想想这是一种典型的生产者消费者模式。生产者是libuv中的线程池,线程池通对io处理返回数据给观察者,实现循环检查观察者来获取返回数据来操作,那么事件循环中检查观察者的线程就是一个消费者。所以这是一个典型的生产者消费者模式

当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序六个循环阶段,

 

timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;

I/O callbacks 阶段: This phase executes callbacks for some system operations such as types of TCP errors. For example if a TCP socket receives ECONNREFUSED when attempting to connect, some *nix systems want to wait to report the error. This will be queued to execute in the I/O callbacks phase;

idle, prepare 阶段: 仅node内部使用;

poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;

check 阶段: 执行setImmediate() 设定的callbacks;

close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.

 

event loop按顺序执行上面的六个阶段,每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时,node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时,event loop会转入下一下阶段.

 

那么我们平常的异步io是在哪个阶段执行的呢,答案是poll阶段。

 

poll阶段

在node.js里,除了上面几个特定阶段的callback之外,任何异步方法完成时,都会将其callback加到poll queue里。分以下的两种情况:

1.当event loop到poll阶段时,且不存在timer,将会发生下面的情况

如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;

如果poll queue为空,将会发生下面情况:

如果代码已经被setImmediate()设定了callback 或者有满足close callbacks阶段的callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)

如果代码没有设定setImmediate(callback)或者没有满足close callbacks阶段的callback,event loop将阻塞在该阶段等待callbacks加入poll queue;

2.当event loop到poll阶段时,如果存在timer并且timer未到超时时间,将会发生下面情况:

则会把最近的一个timer剩余超时时间作为参数传入io_poll()中,这样event loop 阻塞在poll阶段等待时,如果没有任何I/O事件触发,也会由timerout触发跳出等待的操作,结束本阶段,然后在close callbacks阶段结束之后会在进行一次timer超时判断

所以实际上,timer检查会发生在两个地方:timers阶段和close callbacks阶段结束之后

 

 

 

应该说,事件循环、观察者、请求对象、I/O线程池,这四者共同组成了Node异步I/O模型的基本要素。

 

 

setTimeout/setInterval

setTimeout和setInterval的表现和实现其实基本相同,不同的只是setInterval会不断重复。在底层实现上他们是创建了一个Timeout的中间对象,并且放到了实现定时器的红黑树中,每一次tick开始时,都会到这个红黑树中检查是否存在超时的回调,如果存在,则一一按照超时顺序取出来进行回调。因此,我们可以得出这样一个结论:

js的定时器是不可靠的。因此单线程的原因,它是基于tick的,每次tick开始时才开始检查是否有超时,如果一个tick耗时过长,在它之后出发的定时回调都将被延迟

timer的效率不是很高,因是从上取下所有超Timer对象,然后依次调用他们的回调方法进行回调

process.nextTick()方法的操作相对较为轻量,每次调用Process.nextTick()方法,只会将回调函数放入队列中,在下一轮Tick时取出执行。定时器采用红黑树的操作时间复杂度为o(lg(n)),而nextTick()的时间复杂度为o(1)。相较之下,process.nextTick()更高效。


nextTick函数,会将callback封装为一个obj对象,并且插入到nextTickQueue队列(数组)中。

每次nextTick回调,都会nextTickQueue数组中的回调全部跑完!
 

setImmediate函数,首先把callback封装成了一个immediate对象,然后把它插入到了immediateQueue队列(数组)

两者之间其实是有差别的。区别表现为两点:

1、process.nextTick中回调函数的优先级高于setImmediate,根据我前面写的那篇文章可知,原因在于事件循环对观察者的检查是有先后顺序的,process.nextTick属于idle观察者,setImmediate属于check观察者。在每一轮循环检查中,idle观察者先于I/O观察者,I/O观察者先于check观察者。



2、在实现上,process.nextTick的回函数保存在一个数中,setImmediate则保存在一个链表中。顺便这里抛出一个朴灵老师在《深入浅出Node.js》中对process.nextTick和setImmediate的不够准确的描述:“在行为上,process.nextTick在每轮循环中将数组中的回调函数全部执行完,而setImmediate在每轮循环中执行链表中的一个回调函数。


 

3setImmediate可以使用clearImmediate清除(没搞懂个到底能干明白我一下),process.nextTick不能被清除

观察者优先级

在每次轮训检查中,各观察者的优先级分别是:

idle观察者 > I/O观察者 > check观察者。

idle观察者:process.nextTick

I/O观察者:一般性的I/O回调,如网络,文件,数据库I/O等

check观察者:setImmediate,setTimeout


知乎上曾有人贴过一段关于setImmediatesetTimeout(xxx,0)的代,得出了一个这样结论而在setImmedia时,setTimeout是随机的插入在setImmediate序中的。我对这结论是持怀度的

根本原因是node底层的设计所致,也就是说setTimeout(xxx,0)其实在底层强制设置成等同于setTimeout(xxx,1)。小于1秒都要强制设置成1秒

那就很容易理解知乎这位作者的给出的代码为什么是这样的结果了。因此:setTimeout的优先级高于setImmediate,但是因为setTimeout的after被强制修正为1,这就可能存在下一个tick触发时,耗时尚不足1ms,setTimeout的回调依然未超时,因此setImmediate就先执行了!

 

优先级顺序:process.nextTick > setTimeout/setInterval > setImmediate

setTimeout需要使用红黑树,且after设置为0,其实会被node强制转换为1,存在性能上的问题,建议替换为setImmediate

process.nextTick有一些比较难懂的问题和隐患,从0.8版本开始加入setImmediate,使用时,建议使用setImmediate


 

猜你喜欢

转载自blog.csdn.net/u010365819/article/details/84065485