写node.js有一段时间了,一直在理解event loop这个概念,国内外的文章翻阅了也不少,但是对event loop能讲解清楚的还是不多。 最后还是查阅的nodejs对event loop的官方解释才恍然大悟,如获至宝。
写这篇文章的目的是将自己对该文章的理解做一个记录,官方文档链接The Node.js Event Loop, Timers, and process.nextTick()
文章内容可能有错误理解的地方,希望能和大家探讨一下,欢迎批评指正!
Node.js Event Loop 的理解 Timers,process.nextTick()
Event Loop的解释
英文原文: When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.
当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序六个循环阶段,
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
- timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
- I/O callbacks 阶段: 执行除了 close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks;
- idle, prepare 阶段: 仅node内部使用;
- poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;
- check 阶段: 执行setImmediate() 设定的callbacks;
- close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.
每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时, node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时, event loop会转入下一下阶段.
注意上面六个阶段都不包括 process.nextTick()
poll阶段
poll阶段是衔接整个event loop各个阶段比较重要的阶段,为了便于后续例子的理解,本文和原文的介绍顺序不一样,本文先讲这个阶段;
在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行。
poll 阶段有两个主要的功能:
- 处理poll队列(poll quenue)的事件(callback);
- 执行timers的callback,当到达timers指定的时间时;
如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:
- 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
- 如果poll queue为空,将会发生下面情况:
- 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
- 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue;
如果event loop进入了 poll阶段,且代码设定了timer:
- 如果poll queue进入空状态时(即poll 阶段为空闲状态),event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.
以上便是整个event loop时间循环的各个阶段运行机制,有了这层理解,我们来看几个例子
- 注意,例子中给出的时间在不同机器下和同一机器下不同执行时刻,其值都会有差异。
example 1
var fs = require('fs');
function someAsyncOperation (callback) {
// 花费2毫秒
fs.readFile(__dirname + '/' + __filename, callback);
}
var timeoutScheduled = Date.now();
var fileReadTime = 0;
setTimeout(function () {
var delay = Date.now() - timeoutScheduled;
console.log('setTimeout: ' + (delay) + "ms have passed since I was scheduled");
console.log('fileReaderTime',fileReadtime - timeoutScheduled);
}, 10);
someAsyncOperation(function () {
fileReadtime = Date.now();
while(Date.now() - fileReadtime < 20) {
}
});
结果: 先执行someAsyncOperation的callback,再执行setTimeout callback
-> node eventloop.js
setTimeout: 22ms have passed since I was scheduled
fileReaderTime 2
解释: 当时程序启动时,event loop初始化:
- timer阶段(无callback到达,setTimeout需要10毫秒)
- i/o callback阶段,无异步i/o完成
- 忽略
- poll阶段,阻塞在这里,当运行2ms时,fs.readFile完成,将其callback加入 poll队列,并执行callback, 其中callback要消耗20毫秒,等callback之行完,poll处于空闲状态,由于之前设定了timer,因此检查timers,发现timer设定时间是20ms,当前时间运行超过了该值,因此,立即循环回到timer阶段执行其callback,因此,虽然setTimeout的20毫秒,但实际是22毫秒后执行。
example 2
var fs = require('fs');
function someAsyncOperation (callback) {
var time = Date.now();
// 花费9毫秒
fs.readFile('/path/to/xxxx.pdf', callback);
}
var timeoutScheduled = Date.now();
var fileReadTime = 0;
var delay = 0;
setTimeout(function () {
delay = Date.now() - timeoutScheduled;
}, 5);
someAsyncOperation(function () {
fileReadtime = Date.now();
while(Date.now() - fileReadtime < 20) {
}
console.log('setTimeout: ' + (delay) + "ms have passed since I was scheduled");
console.log('fileReaderTime',fileReadtime - timeoutScheduled);
});
结果:setTimeout callback先执行,someAsyncOperation callback后执行
-> node eventloop.js
setTimeout: 7ms have passed since I was scheduled
fileReaderTime 9
解释: 当时程序启动时,event loop初始化:
- timer阶段(无callback到达,setTimeout需要10毫秒)
- i/o callback阶段,无异步i/o完成
- 忽略
- 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
二者非常相似,但是二者区别取决于他们什么时候被调用.
- setImmediate 设计在poll阶段完成时执行,即check阶段;
- setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行
其二者的调用顺序取决于当前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
关于这点的原因,笔者目前还未弄清楚。。。其原因读者可去看@hyj1991 的留言,讲解非常清晰
但当二者在异步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()的时候,来来来喝口水休息休息。
注意:本文讲解的process.nextTick是基于v0.10及以上版本
process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。
var fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
process.nextTick(()=>{
console.log('nextTick3');
})
});
process.nextTick(()=>{
console.log('nextTick1');
})
process.nextTick(()=>{
console.log('nextTick2');
})
});
-> node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout
从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。
example process.nextTick()
当递归调用process.nextTick时,即使fs.readFile完成,其callback无机会执行
var fs = require('fs');
var starttime = Date.now();
var endtime = 0;
fs.readFile(__filename, () => {
endtime = Date.now();
console.log('finish reading time:',endtime - starttime);
});
var index = 0;
function nextTick () {
if (index > 1000) return;
index++;
console.log('nextTick');
process.nextTick(nextTick);
}
nextTick();
-> node eventloop.js
nextTick
nextTick
...
nextTick
nextTick
finish reading time: 246
example setImmediate
将process.nextTick替换成setImmediate后,由于setImmediate只在check阶段执行,那么所有的callback都有机会执行。
var fs = require('fs');
fs.readFile(__filename, () => {
console.log('finish reading');
});
var index = 0;
function Immediate () {
if (index > 100) return;
index++;
console.log('setImmediate');
setImmediate(Immediate);
}
Immediate()
-> node eventloop.js
setImmediate
setImmediate
setImmediate
setImmediate
finish reading time: 19
...
setImmediate
setImmediate
感谢分享
@kevin1211 希望对你有用:)
其实event是由uv_run驱动的,并且是在UV_RUN_ONCE的模式下执行 UV_RUN_ONCE就是你文中描述的
中的“适当条件”,它会阻塞在kqueue的kevent()函数中(这里以mac为例,linux下这一块由epoll_wait()函数处理,windows下则是IOCP) 这里描述UV_RUN_ONCE为适当条件,是因为uv_run还有两种模式,对于UV_RUN_NOWAIT的模式,io_poll不会阻塞,会立即触发超时结束当前的event_loop,进入下一次循环(v4.4.2版本中仅用在进程退出前的最后检查) 实际上,你描述的timer,在一次event loop中可能在两个地方执行:
为什么这么说呢,就算不看源代码,我们也可以思考这样的一个场景: 当你设置了一个超时timer,那么第一次进入uv_run_timer时,超时时间未到;此时进入kevent()阻塞等待 如果此处不提供超时机制,那就会永远阻塞在kevent()的I/O等待中;所以我们在 当前的event loop循环中的某一次时,如果存在timer且为达到设定时间,则会把最近的一个timer剩余超时时间作为参数传入io_poll()中,这样kevent()等待时,如果没有任何I/O事件触发,也会由timerout触发跳出等待的操作,结束本次时间循环 所以呢,在UV_RUN_ONCE的模式下,每次循环结束前,即你说的close callback执行结束后,会再执行一次对timer的超时判断 具体涉及到的代码如下:
对于node事件循环的核心驱动函数uv_run写的一些注释,随便看看吧~
@hyj1991 谢谢你的注释,我只是看了官方文档,加了自己的一些理解,
我会结合你的注释去看看源码。 请教个问题
执行的顺序不确定,就是因为每一次loop,最开始和结束时都检查timer的缘故么?
@vincentLiuxiang 是的,在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进来的时候就有两种情况:
那么你说的为什么在回调中,一定是先immediate执行呢,其实也很容易理解 你可以思考一下你写的场景 由于你的timeout和immediate的事件注册是在readFile的回调执行时,触发的 所以必然的,在readFile的回调执行前的每一次event loop进来的uv_run_timer都不会有超时事件触发 那么当readFile执行完毕,kevent收到监听的fd事件完成后,执行了该回调,此时
所以你会发现,在I/O回调中注册的两者,永远都是immediately先执行
@hyj1991 当时看文档的时候,这块儿就没太弄明白,太感谢了!写的非常清楚。
@vincentLiuxiang 不客气哈,不过我也蛮佩服你的,看文档就能理清这么多哇。我当时是查文档看的晕,资料也少,无奈下直接去读源代码的。。。
感谢分享,精彩的讨论。
楼主真是真知灼见啊
mark
来自酷炫的 CNodeMD
先收藏一波~~~
@JacksonTian 大佬觉得这篇文章说的对不对
文档链接貌似404了。专门去旧分支找链接 https://github.com/nodejs/node/blob/v6.x/doc/topics/event-loop-timers-and-nexttick.md 搜到了这里,拜读了。
@shellphon 老早前写的文章了,谢啦~ 我去重新编辑一下
补充点: 我的疑问:poll 中执行了 i/o callback 为什么还要I/O callbacks。。 “node原理介绍 ” 引用:查阅了libuv 的文档后发现,在 libuv 的 event loop 中,I/O callbacks 阶段会执行 Pending callbacks。绝大多数情况下,在 poll 阶段,所有的 I/O 回调都已经被执行。但是,在某些情况下,有一些回调会被延迟到下一次循环执行。也就是说,在 I/O callbacks 阶段执行的回调函数,是上一次事件循环中被延迟执行的回调函数。 不知道 对不对0 0
hao
@LastKing 你可以参考下源码
额,其实很久以前有个老外讲得还蛮清楚的: http://latentflip.com/loupe/ ,虽然不是专门给 node.js 写的。
@LastKing
严格来说,i/o callbacks并不是处理文件i/o的callback 而是处理一些系统调用错误,比如网络 stream, pipe, tcp, udp通信的错误callback。参考 因为,pending_queue的入列(queue_insert_tail)是通过一个叫 uv__io_feed 的api来调用的 而 uv__io_feed API是在tcp/udp/stream/pipe等相关API调用
@vincentLiuxiang 谢谢 !~ 源码阅读能力不够。多谢 指导!~
运行结果:
@vincentLiuxiang,请问emitter.emit(‘someEvent’);这个的事件处理是在poll阶段吗? 在处理’someEvent’事件的时候是不是都一直在poll阶段,没有阶段切换? 因为如果有阶段切换,那么结果一定是setImmediate先于listener。 不知这样分析是否正确。
@qiulanzhu 不是!
events,timer 不是libuv层面实现的,应该是v8层实现。
所谓 check / poll 阶段是针对libuv层面实现的。
虽然 timer (settimeout) 不是libuv层面实现,但其在uv_run 内执行,因此才有timer阶段。
events注册的事件, 其实是存在了一个eventqueue里,
因此,可以看出,event是同步的!当执行 emit触发时,会先于上述六个阶段执行。
具体例子,和源码分析,等过段时间再贴。。。最尽有点忙
例子没有关于io callback阶段的?为什么readfile 的callback在poll阶段完成?
@isbdnt 等有空了写一个吧。。。
在19楼回复了原理,至于例子,你也可以去写写
也就是说网络io cb在io cb阶段完成吧,并不想自己写,我是伸手党,嘻嘻
@vincentLiuxiang 非常感谢,这样讲清晰多了!
@hyj1991 @vincentLiuxiang
看了好多网上的资料和书,都没用搞懂node是怎样使用epoll完成网络I/O的,大多数资料只重点讲了使用线程池实现文件I/O,但对于网络I/O都是直接说用epoll一带而过。我个人觉得可能是我对epoll不够理解造成的。我不明白的有以下几点: 1.event loop 是不是跑在node主线程上的?执行完同步代码后,再去一遍遍的循环,还是说event loop实际上另外一条线程。 2.网络请求来时候,node会不会再新起一个线程去处理。或者说,用哪个线程去处理,是主线程自行处理吗? 3.epoll会阻塞线程,若使用epoll 的是主线程,那主线程岂不是被epoll阻塞了?如果一直没有网络连接,会一直阻塞到这吗?
@Julyrainy
@Julyrainy epoll主要有三个api
要理解 node 的 epoll 模型,epoll 的原理还是先要去理解的。 这里我简单讲一下:
基于这个基础,那么在node里是怎么做的呢? 我贴一段 libuv的核心代码片段( linux-core.c ),你看看就懂了
epoll_wait 阻塞返回后,回调函数就在uv_run主线程里执行,是没有多线程的. 因此,很容易理解一个现象,当我们在回调函数里执行 while(1) {}, 整个服务都无法响应了。
至于是epoll 好 还是 多线程/线程池好,这个google一下nginx相关文章有很多分析的。 还有我所说的线程是 用户态的应用层 线程, 不是硬件/物理线程。物理线程就那么多,应用层线程池大了,来回切换也是开销。
@hyj1991 @i5ting 也请大神们看看,我理解不一定对。
@vincentLiuxiang 对 epoll 和 uv_run 描述的比较清晰,没问题的。不过 uv_run 实现了跨平台,在 mac 下使用的不是 epoll 而是 kqueue,但是大致的原理是一致的
@hyj1991 嗯啊,我主要分析 linux-core.c , 其他平台我浅以为是类似的,而 epoll 被大家说得最多。 而且还有人把它和多线程混在一起谈。epoll 在 linux 老版本里多线程有惊群现象,在内核 3.x 还是 4.x 版本(具体忘了)有个内核设置可以解决该问题。
@i5ting 多谢狼叔指点!
@vincentLiuxiang 多谢您的耐心指点,我大概已经有些了解了。也就是说epoll在等待网络I/O数据就绪的时候,一直是阻塞的,此时无法响应如文件I/O和计时器事件,直到所有网络I/O完成或者超时,才能继续event loop是吗?
@Julyrainy 下一份 node 源码,自己编译/运行printf一下,什么都清晰了。
@vincentLiuxiang ok,主要是我基础比较差,想先了解一下原理再去看源码,看来我还是得死死地啃一遍源码,多谢您耐心的回答。
c的高手 搞js 很简单呀
请问, node事件循环中是否有体现浏览器 宏任务(macrotask) 和 微任务(microtask) 机制的地方? 如果有, 它与上述文中所说的node事件循环机制有何联系? 如何把他们串起来、讲得通? 浏览器中的macrotask、microtask 知乎macrotask、microtask 谢谢!
@MrStronger 突然发现有5个月没上 cnode 了,两者没关系。。。
@vincentLiuxiang @hyj1991 大佬好 请问 如果process.nextTick是在事件循环的每个phase结束后执行,那么这样的一段代码:
为什么process.nextTick总是第一个执行? 已知在第一次进入event loop的时候,有可能准备时间大于1ms,于是uv_run_timer调用回调函数,那不是应该先执行setTimeout,然后在切换到下一个phase的中间来执行process.nextTick吗? 还是说在每次进入event loop的时候先会去执行一次process.nextTick,然后再开始update_time,进入timer阶段?
@ProfutW @ProfutW 进入 event loop 前会清空一次 next tick queue 的
来自酷炫的 CNodeMD
@ProfutW 具体实现没记错的话在 lib/module.js 的 runMain 方法里面
来自酷炫的 CNodeMD
@hyj1991 好的 谢谢大佬了 我去看看
@vincentLiuxiang @hyj1991 有一段代码的运行结果始终不能理解。 setTimeout(function () { console.log(‘setTimeout1’) Promise.resolve().then(function () { console.log(‘promise’) }); }) setTimeout(function () { console.log(‘setTimeout2’); });
我预想的输出应该是:setTimeout1, promise, setTimeout2 但是实际结果是:setTimeout1, promise, setTimeout2 或者 setTimeout1, setTimeout2, promise。 请问这该如何理解
@hyj1991 @vincentLiuxiang 有个疑惑,按照这个说法,
timer阶段之后是poll/check,下次的timer应该在check之后 输出的顺序也应该是固定的
immediate,timeout,但是实测顺序还是不确定的来晚了,虽然有些还是不懂,但感谢