前端学习后端资源精选IT

简单-Node.js Event Loop 的理解 Timers

2016-11-29  本文已影响294人  Www刘

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

poll 阶段有两个主要的功能
1 处理poll队列(poll quenue)的事件(callback);
2 执行timers的callback,当到达timers指定的时间时;

如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:

如果event loop进入了 poll阶段,且代码设定了timer:

以上便是整个event loop时间循环的各个阶段运行机制,有了这层理解,我们来看几个例子
注意,例子中给出的时间在不同机器下和同一机器下不同执行时刻,其值都会有差异
example 1

解释:
当时程序启动时,event loop初始化:

1 timer阶段(无callback到达,setTimeout需要10毫秒)
2 i/o callback阶段,无异步i/o完成
3 忽略
4 poll阶段,阻塞在这里,当运行2ms时,fs.readFile完成,将其callback加入 poll队列,并执行callback, 其中callback要消耗20毫秒,等callback之行完,poll处于空闲状态,由于之前设定了timer,因此检查timers,发现timer设定时间是20ms,当前时间运行超过了该值,因此,立即循环回到timer阶段执行其callback,因此,虽然setTimeout的20毫秒,但实际是22毫秒后执行。

example 2


解释:
当时程序启动时,event loop初始化:

1 timer阶段(无callback到达,setTimeout需要10毫秒)
2 i/o callback阶段,无异步i/o完成
3 忽略
4 poll阶段,阻塞在这里,当运行5ms时,poll依然空闲,但已设定timer,且时间已到达,因此,event loop需要循环到timer阶段,执行setTimeout callback,由于从poll --> timer中间要经历check,close阶段,这些阶段也会消耗一定时间,因此执行setTimeout callback实际是7毫秒 然后又回到poll阶段等待异步i/o完成,在9毫秒时fs.readFile完成,其callback加入poll queue并执行。

setTimeout 和 setImmediate
二者非常相似,但是二者区别取决于他们什么时候被调用.

其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的

setTimeout(function timeout () { console.log('timeout');},0);
setImmediate(function immediate () { console.log('immediate');});

$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

关于这点的原因
在node中,setTimeout(cb, 0) === setTimeout(cb, 1);
而setImmediately属于uv_run_check的部分
确实每次loop进来,都是先检查uv_run_timer的,但是由于cpu工作耗费时间,比如第一次获取的hrtime为0
那么setTimeout(cb, 1),超时时间就是loop->time = 1(ms,node定时器精确到1ms,但是hrtime是精确到纳秒级别的)
所以第一次loop进来的时候就有两种情况:
1.由于第一次loop前的准备耗时超过1ms,当前的loop->time >=1 ,则uv_run_timer生效,timeout先执行
2.由于第一次loop前的准备耗时小于1ms,当前的loop->time = 0,则本次loop中的第一次uv_run_timer不生效,那么io_poll后先执行uv_run_check,即immediate先执行,然后等close cb执行完后,继续执行uv_run_timer

那么你说的为什么在回调中,一定是先immediate执行呢,其实也很容易理解你可以思考一下你写的场景
由于你的timeout和immediate的事件注册是在readFile的回调执行时,触发的所以必然的,在readFile的回调执行前的每一次event loop进来的uv_run_timer都不会有超时事件触发
那么当readFile执行完毕,kevent收到监听的fd事件完成后,执行了该回调,此时
1.timeout事件注册
2.immediate事件注册
3.由于readFile的回调执行完毕,那么就会从uv_io_poll中出来,此时立即执行uv_run_check,所以immediate事件被执行掉
4.最后的uv_run_timer检查timeout事件,执行timeout事件

所以你会发现,在I/O回调中注册的两者,永远都是immediately先执行

但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

var fs = require('fs')fs.readFile(__filename, () => 
{ setTimeout(() => {
 console.log('timeout')
 }, 0) 
setImmediate(() => { 
console.log('immediate') 
})})
$ node timeout_vs_immediate.js
immediate
timeout

理解了event loop的各阶段顺序这个例子很好理解:因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout

process.nextTick()###

process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。
![]6@4Y@3JMH(~95A`1.png](http://upload-images.jianshu.io/upload_images/1058258-4edc9f71a2b32cda.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
从poll —> check阶段,先执行process.nextTick,
nextTick1
nextTick2
然后进入check,setImmediate,
setImmediate
执行完setImmediate后,出check,进入close callback前,执行process.nextTick
nextTick3
最后进入timer执行setTimeout
setTimeout

process.nextTick()是node早期版本无setImmediate时的产物,node作者推荐我们尽量使用setImmediate。


功能上
这两个都能接受一个传入的function()作为参数,延迟执行。
但是在行为上
process.nextTick()在每轮事件循环中会将数组中的回调函数全部执行,而setImmediate()只会执行链表中的一个回调函数。
但是这就带来了一个问题,使用process.nextTick()推入的回调函数将会顺序执行,在数组中的回调函数执行完之前,都不会进入下次事件循环,如果数组中有一个回调函数执行时间很长,那么其他正在等待执行的回调函数就会处于长时间等待的状态。
因此,当我们需要避免这种I/O回调函数因为process.nextTick()而处于长时间等待的情况时,我们应该使用setImmediate()执行需要延迟执行的任务,因为它在每轮事件循环中只会执行一个回调函数。

上一篇下一篇

猜你喜欢

热点阅读