关于 event loop,我有点懵。

2019-02-19 15:45:40 +08:00
 daguaochengtang

最近在看 event loop,然后为了搞清楚,写了几行代码,发现输出的结果与我理解的不一样。

上代码:

输出结果:

代码贴出来:

var fs = require('fs');

fs.readFile('test.txt', function(err, data){
  console.log('io')
})

setTimeout(function(){
  console.log('timer')
})

var a;
console.time('process')
for(var i = 9000000000; i > 0; i--) {
  a = 1;
}
console.timeEnd('process')

setImmediate(function(){
  console.log('setImmediate')
})

我的理解:

  1. 走到第 3 行,fs 异步操作进入 Event Table,注册相应函数
  2. 走到第 7 行,setTimeout 进入 Event Table,注册相应函数
  3. 走到 for 循环,虽然是同步代码,但是耗时较长。在 for 循环跑完之前,setTimeout 应该先计时完成,把注册的函数移入宏任务队列,随后 fs 的异步事件也完成,把相应的函数移入宏任务队列。
  4. for 循环终于跑完了(从打印结果看大概 10s 左右),代码走到 18 行,setImmediate 进入 Event Table,注册相应函数,随后把相应函数移入宏任务队列。
  5. 至此,第一轮事件循环结束,宏任务队列里的先后顺序应该是 timer,io,setImmediate。但是实际打印的顺序却是 timer,setImmediate,io。请问我在哪一步理解出错了。
5174 次点击
所在节点    Node.js
22 条回复
lhx2008
2019-02-19 15:54:25 +08:00
for 循环代码同步的,等于把 eventloop 阻死了,eventloop 没有执行回调的机会。但是 read 在别的线程做的,肯定已经就绪了,然后插入到 eventloop 队列。但是没有机会执行。
lhx2008
2019-02-19 15:58:27 +08:00
看错了,不好意思
daguaochengtang
2019-02-19 16:02:06 +08:00
@lhx2008 你看我这样理解对不对:for 循环还在跑的过程中,read 实际上已经完成了注册,并且已经加入了 eventloop 队列。此时 eventloop 队列里应该有 timer 和 io 这两个函数(timer 0 大约 4ms 完成肯定比 io 快)。到这一步是否正确?

for 循环结束后,setImmediate 才加入 eventloop,此时 eventloop 里的任务按顺序应该是 timer,io,setImmediate 才对啊?
mrzjd
2019-02-19 16:05:28 +08:00
nodejs 中的事件队列执行顺序与浏览器中有所区别,不是根据回调函数进入队列的时间来决定的。每一轮的时间循环,都有固定的类别的执行顺序,每一类 task 再根据入队列时间顺序执行。可以参考这个文章 https://www.jianshu.com/p/deedcbf68880
daguaochengtang
2019-02-19 16:08:14 +08:00
@mrzjd 这一点我在其它文章里也看到过,可是按照这个类别来推,IO 也是在 immediate 之前执行的啊,为什么输出的是 immediate 先执行呢?
mrzjd
2019-02-19 17:21:57 +08:00
setImmediate 在官方文档中已写明,是在当前时间循环回合结束是调用。但是实际测试会发现,首次执行存在 timer 先于 immediate 执行的情况,阮一峰大大的文章也有说明首次存在随机现象,与机器性能有关。所以你可以把你的代码包裹在一个 timeout 中执行,执行结果就会稳定先执行 setImmediate > timer,至于 io 的执行顺序按照你可以多执行几次,会出现先于 io > setImmediate > timer,也会出现 setImmediate > timer > io 情况,所以 io 的完成时间是不可预估的(这边是我自己的理解)。关于出现你的这个现象,是因为阻塞了进程,导致了 timer>immediate。
mrzjd
2019-02-19 17:37:03 +08:00
@nikolausliu 哇,这边测试的时候居然放在 timeout 中也出现了,timer > immediate 的情况。看来这边的执行情况确实有问题。只能求助其他大大,我去 github issus 找找看再来。
mortonnex
2019-02-19 17:39:18 +08:00
还以为是 netty 里面的 eventLoop...
xmadi
2019-02-19 20:32:11 +08:00
之前看过
setImmediate() 和 settimeout(,0) 和 process.nextTick()还有 new Promise() 这几个方法的立即回调函数执行顺序 和代码顺序无关
mrzjd
2019-02-19 20:37:04 +08:00
@nikolausliu 哈喽,我这边查了 nodejs 的官方英文文档。关于 eventloop 执行流程,我这边的理解如下。首先是 timer 阶段,执行 timer queue 中的函数。然后是 sys pending 阶段,就不多介绍了,主要是执行系统回调。然后是最重要的 polling 阶段,该阶段的两个主要功能就是一根据 timer 所指定的时间阈值和 io 计算 polling 时间,二执行 poling queue 中出现的 io 等回调函数。polling 的流程文档有部分逻辑描述不是很清晰,大致如下,在没有 timer 的情况下,判断队列是否为空,则阻塞等待;如果不为空,则执行其中的值。当执行完毕时,也就是队列为空时,再次判断所执行的脚本中是否有 timer,有 immediate timer 则进入 check 阶段执行,有到期的 timeout timer 则重回 timer 阶段执行。至此 polling 阶段结束。后面的 check 和 close 阶段不多介绍。

那你遇到的问题可以这样解释,程序进入主程序执行,初始化事件循环。timer 放入 timer queue,发起 file io 注册回调,for 循环阻塞进程,immediate 放入 immediate queue。主程序执行完毕,进入事件循环。timer 中执行已到期函数(这里因为执行主程序时阻塞了一段时间,所以 timer 肯定到期了。不然这里就存在 timer 到没到期,影响 immediate 与 timer 的输出顺序),输出 timer,然后 immediate 中有值,执行。然后等待 io 或执行 polling queue。最后再次去 immediate 和 timer 中判断是否有待执行函数。所以输出顺序是 timer immediate io.

这也和官方文档中的示例所吻合,在 setTimeout 中再设置一个 timer 和 immediate,timer 阶段 parent timer 执行,进入 polling 时会根据 childtimer 来计算时间,此时没有 io script 执行,所以 timer 有可能执行的很快到期了,也有可能没有到期,然后进入 check 阶段执行。所以出现了随机顺序的输出。官网中的 io polling 中有 script 设置了 immediate,所以稳定先输出 immediate,而无论 timer 是否到期了都是第二轮以后的 tick 中的 timer 执行。

所以上述两个 demo 程序的分歧点就在于,进入后 polling 最开始的逻辑是否要检测 immediate 是否存在。根据结果来说,第一次 tick 需要检测并执行,后面 tick 不需要。根据有些文章说明,底层的 libuv 确实存在三种 tick 类型的区别。这边因为小的对 C 的知识也不太熟悉,只能后续再求解了。

附上官方文档,https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

写的有点多了,后续整理成文章。有没说清,或理解不正确的地方,还请多多指教。
dcatfly
2019-02-20 00:52:00 +08:00
这个问题有点意思。我觉得楼上 @mrzjd 说的贴边,但还是有些偏差。
首先要明白 node 中 event loop 和浏览器中的 event loop 是不同的。详细说明推荐[官方文档]( https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/)和[这篇文章]( http://lynnelv.github.io/js-event-loop-nodejs).
有了对 node 的 event loop 模型的了解之后,对应到上文的代码中,顺序执行完之后,应该会进入到 timers 阶段。这个时候因为 setTimeout 早就到期了,所以会输出‘ timer ’,然后会走到 poll 阶段,这里按照官方的说法应该是先看有没有 I/O 的回调,如果有回调执行回调队列,执行完毕之后再去看有没有 setImmediate 的回调。所以按照这个流程,加上执行这么久了,I/O 应该走完了,那么应该是先输出‘ io ’,然后输出‘ setImmediate ’。但实际上输出的顺序是相反的。
这里我的猜测是 fs.readFile 不止是一个 I/O 异步回调,它可能包含了多个 I/O 异步。比如说读取之前要先看文件能不能读之类的。所以在 poll 阶段时执行了 fs.readFile 中的第一个异步 I/O,但还没有真正的读取文件,然后又到了 check 阶段执行了 setImmediate, 重新进入了 event loop.
而且当 fs.readFile 换成 fs.stat 时,输出的顺序符合期望。我觉得这也验证了我的猜测。
daguaochengtang
2019-02-20 09:07:22 +08:00
@mrzjd
@dcatfly
哇,非常感谢你们打了这么多字回复我。昨天下班以后就没上 v 站了。今天一上班就看到了。我先看下你们给我的回复,好好捋一捋,再次感谢!
daguaochengtang
2019-02-20 09:16:40 +08:00
@mrzjd
@dcatfly
我之前的理解,是按照浏览器中的 event loop 模型来理解的,看了你们的解答,好像有点明白了,但还是有点模糊。node 环境下的 event loop 还是有些复杂的,我再多查查资料,争取今天把这个彻底搞清楚,不然下次遇到这种问题还是懵逼。可惜我英语太渣了,英语好的话直接看官方文档就好了。
daguaochengtang
2019-02-20 09:26:58 +08:00
@dcatfly
我按照你说得把 fs.readFile 换成 fs.stat 输出还是和预期不符。我的 node 版本是 8.10.0,难道说不同版本的 event loop 的实现是不同的?
![]( )
mrzjd
2019-02-20 09:58:18 +08:00
@dcatfly 哈喽,如果说 fs.readFile 可能包含多个 IO 异步,确实可以解释 immediate 出现在 io 之前输出的现象。但是我这边把 fs.readFile 替换成 fs.stat 依然是 timer > immediate > io。至此根据官方文档的说明,tcp ECONNREFUSED 的顺序属于 pending callbacks 阶段,我这边添加了一个无效的 tcp 请求,输出顺序是 timer > immediate > io > net error。不知是否是我的代码有误,可能分享一下你的具体的输出 stat > immediate 的代码。

此外官网中 polling 阶段进入时有这样的描述

When the event loop enters the poll phase _and there are no timers scheduled_, one of two things will happen:

这好像没有涉及到如果还有 timers 存在的话情况,因为下面有描述到 immediate 也是一种特殊的 timers。这因此我才有猜想第一次 polling 会检查 timers。不知道这边的逻辑具体是怎样的。
daguaochengtang
2019-02-20 10:35:00 +08:00
@mrzjd
@dcatfly
fs.readFile 即使包含多个 IO 异步,在同步代码跑完之前也肯定已经完成了,你们可以看下我#14 楼里的截图,红色框里的输出显示,for 循环跑了 10681.673ms,一共 10s 左右啊,怎么也够一个 readFile 完成的了(我 readFile 里就几个字母)。所以我觉得问题肯定步出在这。
mrzjd
2019-02-20 12:02:13 +08:00
@nikolausliu dcatfly 的意思是 stat 中嵌入 read 的异步操作,所以第一轮完成的是 stat 查询文件状态,这时再去发送读取文件的异步,所以可能导致读取的回调在第二轮 polling 中执行。最终就导致了,immediate > io。但是现在 stat 的回调也是后于 immediate 执行。所以应该与 readfile 是否包含多个 io 异步没关系。
zhangzhen
2019-02-20 13:23:39 +08:00
需要注意 node 的 event loop 与 浏览器不同。
可以参考这个图: https://cdn-images-1.medium.com/max/800/1*aU5dr98pxTsZ4AMfnA6lNA.png
以及 这个博客系列: https://jsblog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810

for 是同步代码,所以 process 首先输出
setTimeout 最先执行,timer 进入下一次 EventLoop 队首
setImmediate 执行阶段位于 timer 之后
IO 的总时间最长,最后输出
dcatfly
2019-02-20 23:21:35 +08:00
@mrzjd
我就是 copy 楼主的代码,只把 readFile 改成了 stat,测试多次结果相同。node 版本也改为了跟楼主一样的 8.10.0,电脑环境为 mac,但是这个应该平台无关。
另外如果 readFile 的 path 是错误的,打印的顺序也符合期望,似乎也证实了我的猜测。
你说的如果进入 poll 的时候 timers 还存在的情况文档中确实没说。我也没找到明确的资料。但是我觉得这个 timers 指的是第一个 timers 阶段中的 timers,不包含 immediate。
原文确实有说 immediate 也是一种特殊的 timers,但是原文是说它是一个特殊的 timers 并且在 event loop 中单独的阶段运行。
setImmediate() is actually a special timer that runs in a separate phase of the event loop.
setImmediate 在 check 阶段运行应该是没有疑问的。
daguaochengtang
2019-02-21 10:32:02 +08:00
@dcatfly
@mrzjd
我在另一篇 v 友的帖子里求助了,你们可以看下他的解答:
https://www.v2ex.com/t/536121#reply3

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/536584

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX