[NodeJs系列]Q&A之理解NodeJs中的Event LoopTimers以及process.nextTick()

Posted 前端神盾局

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[NodeJs系列]Q&A之理解NodeJs中的Event LoopTimers以及process.nextTick()相关的知识,希望对你有一定的参考价值。

在上一篇文章中笔者提了几个问题,现在针对这些问题给出我的理解,如有错漏烦请指正。

poll阶段什么时候会被阻塞?

在上一篇文章中提到在poll阶段会“接收新的I/O事件并且在适当时node会阻塞在这里”,那什么情况下会阻塞呢?阻塞多久呢?

对于这个问题,我们必须深入到libuv的源码,看看poll阶段是怎么实现的:

 
   
   
 
  1. int uv_run(uv_loop_t* loop, uv_run_mode mode) {

  2.  int timeout;

  3.  int r;

  4.  int ran_pending;


  5.  r = uv__loop_alive(loop);

  6.  if (!r)

  7.    uv__update_time(loop);


  8.  while (r != 0 && loop->stop_flag == 0) {

  9.    uv__update_time(loop);

  10.    uv__run_timers(loop);

  11.    ran_pending = uv__run_pending(loop);

  12.    uv__run_idle(loop);

  13.    uv__run_prepare(loop);


  14.    timeout = 0;

  15.    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)

  16.      timeout = uv_backend_timeout(loop);


  17.    // 这是poll阶段

  18.    uv__io_poll(loop, timeout);

  19.    uv__run_check(loop);

  20.    uv__run_closing_handles(loop);


  21.    if (mode == UV_RUN_ONCE) {

  22.      /* UV_RUN_ONCE implies forward progress: at least one callback must have

  23.       * been invoked when it returns. uv__io_poll() can return without doing

  24.       * I/O (meaning: no callbacks) when its timeout expires - which means we

  25.       * have pending timers that satisfy the forward progress constraint.

  26.       *

  27.       * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from

  28.       * the check.

  29.       */

  30.      uv__update_time(loop);

  31.      uv__run_timers(loop);

  32.    }


  33.    r = uv__loop_alive(loop);

  34.    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)

  35.      break;

  36.  }


  37.  /* The if statement lets gcc compile it to a conditional store. Avoids

  38.   * dirtying a cache line.

  39.   */

  40.  if (loop->stop_flag != 0)

  41.    loop->stop_flag = 0;


  42.  return r;

  43. }

从源码我们可以看到 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值的问题,根据官方文档 mode一共有三种情况:

  • UV_RUN_DEFAULT

  • UV_RUN_ONCE

  • UV_RUN_NOWAIT

这里我们只关心 UV_RUN_DEFAULT,因为Node event loop使用的是这种模式.

OK~回到问题,我们再看一下 uv_backend_timeout会返回什么?

 
   
   
 
  1. int uv_backend_timeout(const uv_loop_t* loop) {

  2.  if (loop->stop_flag != 0)

  3.    return 0;


  4.  if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))

  5.    return 0;


  6.  if (!QUEUE_EMPTY(&loop->idle_handles))

  7.    return 0;


  8.  if (!QUEUE_EMPTY(&loop->pending_queue))

  9.    return 0;


  10.  if (loop->closing_handles)

  11.    return 0;


  12.  return uv__next_timeout(loop);

  13. }

这是一个多步条件判断函数,我们一个个分析:

  1. 如果event loop已(或正在)结束(调用了 uv_stop(), stop_flag!=0), timeout为0

  2. 如果没有异步任务需要处理, timeout为0

  3. 如果还有未处理的 idle_handles和 pending_queue, timeout为0(对于 idle_handles和 pending_queue分别代表什么,笔者还没有概念,如果后面有相应资料会及时更新)

  4. 如果还有存在未清理的资源, timeout为0

  5. 如果以上条件都不满足,则使用 uv__next_timeout处理

 
   
   
 
  1. int uv__next_timeout(const uv_loop_t* loop) {

  2.  const struct heap_node* heap_node;

  3.  const uv_timer_t* handle;

  4.  uint64_t diff;


  5.  heap_node = heap_min((const struct heap*) &loop->timer_heap);

  6.  if (heap_node == NULL)

  7.    return -1; /* block indefinitely */


  8.  handle = container_of(heap_node, uv_timer_t, heap_node);

  9.  if (handle->timeout <= loop->time)

  10.    return 0;


  11.  // 这句代码给出了关键性的指导

  12.  // 对比当前loop的时间戳

  13.  diff = handle->timeout - loop->time;


  14.  //不能大于最大的INT_MAX

  15.  if (diff > INT_MAX)

  16.    diff = INT_MAX;


  17.  return diff;

  18. }

总结一下,event loop 满足以下条件时,poll阶段会进行阻塞:

  1. event loop 并未触发关闭动作

  2. 还有异步队列没有处理

  3. 资源已全部关闭

而阻塞的时间最长不超过给定定时器的最小阀值

为什么在非I/O循环中,setTimeoutsetImmediate的执行顺序是不一定的?

上文提到 setTimeout和 setImmediate在非I/O循环中,执行顺序是不一定的,比如:

 
   
   
 
  1. setTimeout(function timeout() {

  2.  console.log('timeout');

  3. }, 0);


  4. setImmediate(function immediate() {

  5.  console.log('immediate');

  6. });

 
   
   
 
  1. $ node timeout_vs_immediate.js

  2. timeout

  3. immediate


  4. $ node timeout_vs_immediate.js

  5. immediate

  6. timeout

相同代码,两次运行结果却是相反的,这是为什么呢?

在node中, setTimeout(cb,0)===setTimeout(cb,1)

在event loop的第一个阶段(timers阶段),node都会从一堆定时器中取出一个最小阀值的定时器来与 loop->time进行比较,如果阀值小于等于 loop->time表示定时器已超时,相应的回调便会执行(随后会检查下一个定时器),如果没有则会进入下一个阶段。

所以 setTimeout是否在第一阶段执行取决于 loop->time的大小,这里可能出现两种情况:

1. 由于第一次loop前的准备耗时超过1ms,当前的 loop->time>=1 ,则 uv_run_timer生效, timeout先执行

2. 由于第一次loop前的准备耗时小于1ms,当前的 loop->time<1,则本次loop中的第一次 uv_run_timer不生效,那么 io_poll后先执行 uv_run_check,即 immediate先执行,然后等 close cb执行完后,继续执行 uv_run_timer

这就是为什么同一段代码,执行结果随机的缘故。那为什么说在I/O回调中,一定是先 immediate执行呢,其实也很容易理解,考虑以下场景:

 
   
   
 
  1. // timeout_vs_immediate.js

  2. const fs = require('fs');


  3. fs.readFile(__filename, () => {

  4.  setTimeout(() => {

  5.    console.log('timeout');

  6.  }, 0);

  7.  setImmediate(() => {

  8.    console.log('immediate');

  9.  });

  10. });

由于 timeout和 immediate的事件注册是在 readFile的回调执行时触发的,所以必然的,在 readFile的回调执行前的每一次event loop进来的 uv_run_timer都不会有超时事件触发 那么当 readFile执行完毕,poll阶段收到监听的fd事件完成后,执行了该回调,此时

  1. timeout事件注册

  2. immediate事件注册

  3. 由于 readFile的回调执行完毕,那么就会从 uv_io_poll中出来,此时立即执行 uv_run_check,所以 immediate事件被执行掉

  4. 最后的 uv_run_timer检查 timeout事件,执行 timeout事件

所以你会发现,在I/O回调中注册的两者,永远都是 immediately先执行

JS调用栈被展开是什么意思?

栈展开主要是指在抛出异常后逐层匹配catch语句的过程,举个例子:

 
   
   
 
  1. function a(){

  2.  b();

  3. }

  4. function b(){

  5.  c();

  6. }

  7. function c(){

  8.  throw new Error('from function c');

  9. }


  10. a();

这个例子中,函数 c抛出异常,这是首先会在 c函数本身检查是否存在try相关的catch语句,如果没有就退出当前函数,并且释放当前函数的内存并销毁局部对象,继续到 b函数中查找,这个过程就称之为栈展开。

参考

  1. https://zhuanlan.zhihu.com/p/35039878

  2. https://cnodejs.org/topic/57d68794cb6f605d360105bf

  3. http://gngshn.github.io/2017/09/01/libuv-%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90-2-event-loop%E7%9A%84%E8%BF%90%E8%BD%AC/

  4. http://docs.libuv.org/en/v1.x/design.html


以上是关于[NodeJs系列]Q&A之理解NodeJs中的Event LoopTimers以及process.nextTick()的主要内容,如果未能解决你的问题,请参考以下文章

Nodejs 操作Mysql

暑假第十九天之每天一些题系列

小Q系列之 最佳裁判

进阶之初探nodeJS

进阶之初探nodeJS

windows系统下简单nodej.s环境配置 安装