在上一篇文章理解 Node.JS 中的 Event Loop,Timers 以及 process.nextTick 中笔者提了几个问题, 现在针对这些问题给出我的理解, 如有错漏烦请指正.
如果你对 Node.JS 系列感兴趣, 欢迎关注前端神盾局或笔者微信 (w979436427) 交流讨论 node 学习心得
poll 阶段什么时候会被阻塞?
在上一篇文章中提到在 poll 阶段会 "接收新的 I/O 事件并且在适当时 node 会阻塞在这里", 那什么情况下会阻塞呢? 阻塞多久呢?
对于这个问题, 我们必须深入到 libuv 的源码, 看看 poll 阶段是怎么实现的:
- int uv_run(uv_loop_t* loop, uv_run_mode mode) {
- int timeout;
- int r;
- int ran_pending;
- r = uv__loop_alive(loop);
- if (!r)
- uv__update_time(loop);
- while (r != 0 && loop->stop_flag == 0) {
- uv__update_time(loop);
- uv__run_timers(loop);
- ran_pending = uv__run_pending(loop);
- uv__run_idle(loop);
- uv__run_prepare(loop);
- timeout = 0;
- if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
- timeout = uv_backend_timeout(loop);
- // 这是 poll 阶段
- uv__io_poll(loop, timeout);
- uv__run_check(loop);
- uv__run_closing_handles(loop);
- if (mode == UV_RUN_ONCE) {
- /* UV_RUN_ONCE implies forward progress: at least one callback must have
- * been invoked when it returns. uv__io_poll() can return without doing
- * I/O (meaning: no callbacks) when its timeout expires - which means we
- * have pending timers that satisfy the forward progress constraint.
- *
- * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
- * the check.
- */
- uv__update_time(loop);
- uv__run_timers(loop);
- }
- r = uv__loop_alive(loop);
- if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
- break;
- }
- /* The if statement lets gcc compile it to a conditional store. Avoids
- * dirtying a cache line.
- */
- if (loop->stop_flag != 0)
- loop->stop_flag = 0;
- return r;
- }
从源码我们可以看到 uv__io_poll 传入了 timeout 作为参数, 而这个 timeout 就决定了 poll 阶段阻塞的时长, 明白这一点我们就可以把问题转化成: 是什么决定的 timeout 的值?
再回到源码中, timeout 的初始值为 0, 也就意味着 poll 阶段之后会直接转入 check 阶段而不会发生阻塞. 但是当(mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT 这些条件成立时, timeout 就由 uv_backend_timeout 的返回值决定.
这里需要插播一下关于 mode 值的问题, 根据官方文档 http://docs.libuv.org/en/v1.x/loop.html#c.uv_run mode 一共有三种情况:
- UV_RUN_DEFAULT
- UV_RUN_ONCE
- UV_RUN_NOWAIT
这里我们只关心 UV_RUN_DEFAULT, 因为 Node event loop 使用的是这种模式.
OK~回到问题, 我们再看一下 uv_backend_timeout 会返回什么?
- int uv_backend_timeout(const uv_loop_t* loop) {
- if (loop->stop_flag != 0)
- return 0;
- if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
- return 0;
- if (!QUEUE_EMPTY(&loop->idle_handles))
- return 0;
- if (!QUEUE_EMPTY(&loop->pending_queue))
- return 0;
- if (loop->closing_handles)
- return 0;
- return uv__next_timeout(loop);
- }
这是一个多步条件判断函数, 我们一个个分析:
如果 event loop 已 (或正在) 结束(调用了 uv_stop(),stop_flag != 0),timeout 为 0
如果没有异步任务需要处理, timeout 为 0
如果还有未处理的 idle_handles 和 pending_queue,timeout 为 0(对于 idle_handles 和 pending_queue 分别代表什么, 笔者还没有概念, 如果后面有相应资料会及时更新)
如果还有存在未清理的资源, timeout 为 0
如果以上条件都不满足, 则使用 uv__next_timeout 处理
- int uv__next_timeout(const uv_loop_t* loop) {
- const struct heap_node* heap_node;
- const uv_timer_t* handle;
- uint64_t diff;
- heap_node = heap_min((const struct heap*) &loop->timer_heap);
- if (heap_node == NULL)
- return -1; /* block indefinitely */
- handle = container_of(heap_node, uv_timer_t, heap_node);
- if (handle->timeout <= loop->time)
- return 0;
- // 这句代码给出了关键性的指导
- // 对比当前 loop 的时间戳
- diff = handle->timeout - loop->time;
- // 不能大于最大的 INT_MAX
- if (diff> INT_MAX)
- diff = INT_MAX;
- return diff;
- }
总结一下, event loop 满足以下条件时, poll 阶段会进行阻塞:
event loop 并未触发关闭动作
还有异步队列没有处理
资源已全部关闭
而阻塞的时间最长不超过给定定时器的最小阀值
为什么在非 I/O 循环中, setTimeout 和 setImmediate 的执行顺序是不一定的?
上文 https://mp.weixin.qq.com/s/XHRae8YxazD21cMLSpAv1Q 提到 setTimeout 和 setImmediate 在非 I/O 循环中, 执行顺序是不一定的, 比如:
- 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)
在 event loop 的第一个阶段(timers 阶段),node 都会从一堆定时器中取出一个最小阀值的定时器来与 loop->time 进行比较, 如果阀值小于等于 loop->time 表示定时器已超时, 相应的回调便会执行(随后会检查下一个定时器), 如果没有则会进入下一个阶段.
所以 setTimeout 是否在第一阶段执行取决于 loop->time 的大小, 这里可能出现两种情况:
由于第一次 loop 前的准备耗时超过 1ms, 当前的 loop->time>=1 , 则 uv_run_timer 生效, timeout 先执行
由于第一次 loop 前的准备耗时小于 1ms, 当前的 loop->time <1, 则本次 loop 中的第一次 uv_run_timer 不生效, 那么 io_poll 后先执行 uv_run_check, 即 immediate 先执行, 然后等 close cb 执行完后, 继续执行 uv_run_timer
这就是为什么同一段代码, 执行结果随机的缘故. 那为什么说在 I/O 回调中, 一定是先 immediate 执行呢, 其实也很容易理解, 考虑以下场景:
- // timeout_vs_immediate.JS
- const fs = require('fs');
- fs.readFile(__filename, () => {
- setTimeout(() => {
- console.log('timeout');
- }, 0);
- setImmediate(() => {
- console.log('immediate');
- });
- });
由于 timeout 和 immediate 的事件注册是在 readFile 的回调执行时触发的, 所以必然的, 在 readFile 的回调执行前的每一次 event loop 进来的 uv_run_timer 都不会有超时事件触发 那么当 readFile 执行完毕, poll 阶段收到监听的 fd 事件完成后, 执行了该回调, 此时
timeout 事件注册
immediate 事件注册
由于 readFile 的回调执行完毕, 那么就会从 uv_io_poll 中出来, 此时立即执行 uv_run_check, 所以 immediate 事件被执行掉
最后的 uv_run_timer 检查 timeout 事件, 执行 timeout 事件
所以你会发现, 在 I/O 回调中注册的两者, 永远都是 immediately 先执行
JS 调用栈被展开是什么意思?
栈展开主要是指在抛出异常后逐层匹配 catch 语句的过程, 举个例子:
- function a(){
- b();
- }
- function b(){
- c();
- }
- function c(){
- throw new Error('from function c');
- }
- a();
这个例子中, 函数 c 抛出异常, 这是首先会在 c 函数本身检查是否存在 try 相关的 catch 语句, 如果没有就退出当前函数, 并且释放当前函数的内存并销毁局部对象, 继续到 b 函数中查找, 这个过程就称之为栈展开.
参考
- https://zhuanlan.zhihu.com/p/35039878
- https://cnodejs.org/topic/57d68794cb6f605d360105bf
- http://docs.libuv.org/en/v1.x/design.html
来源: https://juejin.im/post/5c4335d9f265da61530509d1