eventloop参观器取NodeJS条件 eventloop外同详解开(转)

2019-05-13 / 未解之谜 / 0 次阅读 / 0 次评论

如果你已经理解了上面的现象,那我们已经算基本了解 nodejs 的 event loop 了,但是其中还有一点细节

细节一:setTimeout 与 setImmediate 的顺序

本来这不应该成为一个问题,因为在文首显而易见,timers 是在 check 之前的。
但事实上,Node 并不能保证 timers 在预设时间到了就会立即执行,因为 Node 对 timers 的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout() 和 setImmediate() 都写在 Main 进程中,但它们的执行顺序是不确定的:

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

虽然 setTimeout 延时为 0,但是一般情况 Node 把 0 会设置为 1ms,所以,当 Node 准备 event loop 的时间大于 1ms 时,进入 timers 阶段时,setTimeout 已经到期,则会先执行 setTimeout;反之,若进入 timers 阶段用时小于 1ms,setTimeout 尚未到期,则会错过 timers 阶段,先进入 check 阶段,而先执行 setImmediate

但有一种情况,它们两者的顺序是固定的:

const fs = require('fs')
fs.readFile('test.txt', () = {
 console.log('readFile')
 setTimeout(() = {
 console.log('timeout')
 }, 0)
 setImmediate(() = {
 console.log('immediate')

和之前情况的区别在于,此时 setTimeout 和 setImmediate 是写在 I/O callbacks 中的,这意味着,我们处于 poll 阶段,然后是 check 阶段,所以这时无论 setTimeout 到期多么迅速,都会先执行 setImmediate。本质上是因为,我们从 poll 阶段开始执行,而非一个 Tick 的初始阶段

细节二:poll 阶段

poll 阶段主要有两个功能:

获取新的 I/O 事件,并执行这些 I/O 的回调,之后适当的条件下 node 将阻塞在这里 当有 immediate 或已超时的 timers,执行它们的回调

poll 阶段用于获取并执行几乎所有 I/O 事件回调,是使得 node event loop 得以无限循环下去的重要阶段。所以它的首要任务就是同步执行所有 poll queue 中的所有 callbacks 直到 queue 被清空或者已执行的 callbacks 达到一定上限,然后结束 poll 阶段,接下来会有几种情况:

setImmediate 的 queue 不为空,则进入 check 阶段,然后是 close callbacks 阶段 setImmediate 的 queue 为空,但是 timers 的 queue 不为空,则直接进入 timers 阶段,然后又来到 poll 阶段 setImmediate 的 queue 为空,timers 的 queue 也为空,此时会阻塞在这里,因为无事可做,也确实没有循环下去的必要 细节三:关于 pending callbacks 阶段

在很多文章中,将 pending callbacks 阶段都写作 I/O callbacks 阶段,并说在此阶段,执行了除 close callbacks、 timers、setImmediate以外的几乎所有的回调,也就是把 poll 阶段的工作与此阶段的工作混淆了。
在我阅读时,就曾产生过疑问,假如大部分回调是在 I/O callbacks 阶段执行的,那么 poll 阶段就没有理由阻塞,因为你并不能保证 无事可做 ,你得去 I/O callbacks 阶段检查一下才知道嘛!
所以最终结合其他几篇文章以及对源码的分析,应该可以确定,I/O callbacks 更准确的叫做 pending callbacks,它所执行的回调是比较特殊的、且不需要关心的,而真正重要的、大部分回调所执行的阶段是在 poll 阶段。

关于 pending callbacks 有如下说法,可以作为参考

查阅了后发现,在 libuv 的 event loop 中,I/O callbacks 阶段会执行 Pending callbacks。绝大多数情况下,在 poll 阶段,所有的 I/O 回调都已经被执行。但是,在某些情况下,有一些回调会被延迟到下一次循环执行。也就是说,在 I/O callbacks 阶段执行的回调函数,是上一次事件循环中被延迟执行的回调函数。