最近想要深入了解下 NodeJS 中 Event Loop 的工作机制,但网上的文章重复性较高,还派生出一些很容易混淆的概念,而且有些文章里举的例子甚至无法自圆其说。所以自己参照 Node 官方给的一篇介绍文章,和 Medium 上看到的一个系列文章,很好地介绍了 Event Loop 的工作原理,现在把学习笔记做个梳理:
- Node 主要通过 libuv 来实现异步机制,libuv 主要提供了两个功能:
- Event Demultiplexer:代理 Node 发起的异步 I/O 请求,部分功能通过不同 OS 提供的异步机制(如 epoll/kqueue/IOCP 等)来执行,部分没有原生异步 API 支持的 I/O 功能,libuv 实现了一个线程池(默认线程数为4)来实现异步。应尽量避免使用线程池中的线程来执行异步,因为会造成性能问题(也可以通过设置
$UV_THREADPOOL_SIZE
来修改默认线程数)。 - Event Queue:每个 cb 被称为一个 event,libuv 通过设置 Event Loop 执行机制,和多个 Event Queue 来执行异步 cb。
- Event Demultiplexer:代理 Node 发起的异步 I/O 请求,部分功能通过不同 OS 提供的异步机制(如 epoll/kqueue/IOCP 等)来执行,部分没有原生异步 API 支持的 I/O 功能,libuv 实现了一个线程池(默认线程数为4)来实现异步。应尽量避免使用线程池中的线程来执行异步,因为会造成性能问题(也可以通过设置
- libuv 提供的 Event Queue 主要有:
- Expired Timer/Interval Queue:用来放入已经到期的 timers 和 intervals;
- IO Event Queue:已经完成的 IO cb;
- Immediate Queue:执行
setImmediate
添加的 cb; - Close Handlers Queue:Close Event cb;
- 除了上述 libuv 设置的4个主要的 Event Queue,Node 自身还设置了两个 Intermediate Queue 用来执行 microtask:
- Next Tick Queue:执行
process.nextTick
添加的 cb,Node v0.12 之后,取消了添加process.maxTickDepth
限制该类型 cb 执行的数量,只能依靠代码保证; - Other Microtask Queue:用来执行
resolved/rejected
的原生Promise
等(注意,Q/Bluebird 等第三方 Promise 库的执行顺序和原生 Promise 不同)。
- Next Tick Queue:执行
- Event Loop 按顺序执行 libuv 设置的4个 Queue 中的 cb,每一步会将当前队列中的 cb 执行完成,新添加到该 Queue 的 cb 会在下一轮 loop 中执行;同时,在进入每一个 Queue 之前,Event Loop 会检查 Node 设置的2个 Intermediate Queue,并按顺序执行这里两个 Queue 中的 cb,期间新添加到这两个 Queue 中的 cb 会被添加到 Queue 的末尾,在本轮执行(包括执行完 Other Microtask Queue 后,Event Loop 仍然会再 check 下 Next Tick Queue,如果有新 cb,仍会执行)。
- 上述的6个队列,其实对应了有些文章中的 macrotask 和 microtask,前者对应 libuv 设置的4个队列中的 cb,后者对应 Node 设置的2个队列中的 cb。
- timer 和 interval 设置的过期时间并不保证在该时间一定执行 cb,而是保证 cb 的执行时间最早不会早于该限制。
1 2 3 4 5 6 7 |
setTimeout(function() { console.log('setTimeout') }, 0); setImmediate(function() { console.log('setImmediate') }); |
- 由于 timer 的上述特性,使得上述代码中的 timer 可能在第一轮 Event Loop 中执行,也可能在第一轮 Event Loop 中还没有准备好从而放在第二轮执行,所以代码的输出结果是不确定的。
- 但是 Immediate Queue 肯定在 IO Queue 之后执行,所以以下代码的输出顺序是确定的,
setImmediate
设置的 cb 总会先执行:
1 2 3 4 5 6 7 8 9 10 11 |
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout') }, 0); setImmediate(() => { console.log('immediate') }) }); |
- Node 可以看做一个工具集合,其中包括以下几个模块:
- Chrome V8 engine:执行 JS;
- libuv:通过 Event Loop 执行异步操作;
- c-ares:DNS 操作;
- Other add-ons:http-parser/crypto/zlib 等。
- Event Loop 在以下3中情形下不会退出循环:
- Queue 中仍有 cb 待执行;
- 仍有 IO 处于 pending 状态;
- 仍有 Close Event 的 cb 待执行。
- 在进入 Check Pace(
setImmediate
设置 cb 对应的 Queue)之前,libuv 内部还有一个 IO polling 的阶段,它会在某些特定条件下 block,等待 IO 完成;如果任何一个 Event Queue 中有 task,该阶段都不会 block;如果没有带执行的 task,在某些环境变量设置条件下,该阶段会 block 的时间为下一个 timer 待触发的时间,或者到达最大 block 时间限制,之后重新激活 Event Loop。 - 示例1:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
setImmediate(() => console.log('this is set immediate 1')); setImmediate(() => console.log('this is set immediate 2')); setImmediate(() => console.log('this is set immediate 3')); setTimeout(() => console.log('this is set timeout 1'), 0); setTimeout(() => { console.log('this is set timeout 2'); process.nextTick(() => console.log('this is process.nextTick added inside setTimeout')); }, 0); setTimeout(() => console.log('this is set timeout 3'), 0); setTimeout(() => console.log('this is set timeout 4'), 0); setTimeout(() => console.log('this is set timeout 5'), 0); process.nextTick(() => console.log('this is process.nextTick 1')); process.nextTick(() => { process.nextTick(console.log.bind(console, 'this is the inner next tick inside next tick')); }); process.nextTick(() => console.log('this is process.nextTick 2')); process.nextTick(() => console.log('this is process.nextTick 3')); process.nextTick(() => console.log('this is process.nextTick 4')); |
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
this is process.nextTick 1 this is process.nextTick 2 this is process.nextTick 3 this is process.nextTick 4 this is the inner next tick inside next tick this is set timeout 1 this is set timeout 2 this is set timeout 3 this is set timeout 4 this is set timeout 5 this is process.nextTick added inside setTimeout this is set immediate 1 this is set immediate 2 this is set immediate 3 |
示例2:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Promise.resolve().then(() => console.log('promise1 resolved')); Promise.resolve().then(() => console.log('promise2 resolved')); Promise.resolve().then(() => { console.log('promise3 resolved'); process.nextTick(() => console.log('next tick inside promise resolve handler')); }); Promise.resolve().then(() => console.log('promise4 resolved')); Promise.resolve().then(() => console.log('promise5 resolved')); setImmediate(() => console.log('set immediate1')); setImmediate(() => console.log('set immediate2')); process.nextTick(() => console.log('next tick1')); process.nextTick(() => console.log('next tick2')); process.nextTick(() => console.log('next tick3')); setTimeout(() => console.log('set timeout'), 0); setImmediate(() => console.log('set immediate3')); setImmediate(() => console.log('set immediate4')); |
执行结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
next tick1 next tick2 next tick3 promise1 resolved promise2 resolved promise3 resolved promise4 resolved promise5 resolved next tick inside promise resolve handler set timeout set immediate1 set immediate2 set immediate3 set immediate4 |
参考文章:
1. Event Loop and the Big Picture — NodeJS Event Loop Part 1
2. Timers, Immediates and Process.nextTick— NodeJS Event Loop Part 2
3. Promises, Next-Ticks and Immediates— NodeJS Event Loop Part 3
4. Handling IO — NodeJS Event Loop Part 4
5. The Node.js Event Loop, Timers, and process.nextTick()
注:转载注明出处并联系作者,本文链接:https://nodefe.com/nodejs-event-loop/
C’est vraiment intéressant, vous êtes un blogueur très compétent. J’ai rejoint votre flux rss et je suis impatient de chercher plus de votre merveilleux message. Aussi, j’ai partagé votre site web dans mes réseaux sociaux!
Frederique Clair Ulysses