hello2dj / blog

一些总结文章
27 stars 1 forks source link

node定时器相关 #38

Open hello2dj opened 5 years ago

hello2dj commented 5 years ago

先来温习一下event loop中的几个phase可参见我的上篇文章libuv概览

为什么会强调上述几个阶段呢?可以参见一下node官网对于event loop的解释

  1. node的定时器setTimeout,setInterval是在timers 阶段检查是否过期执行的
  2. node的setImmediate 是在check阶段执行的
  3. 其他的一些监听文件描述符而执行的cb是在poll(I/O)阶段执行的

其实我们可以总解一下js代码的执行时间有哪些?

  1. event loop 开始之前的代码
  2. event loop 中各个phase执行的代码 也就是说node的js代码会在两种情况下执行(纯属自己的看法)

接下来就是解析一下他们的执行顺序问题

  1. setTimeout(n) VS setImmediate

    setImmediate 一定会在本轮event loop内的check 阶段执行,若是check阶段已经错过了,那就只能在下一轮的check阶段执行了。 参见上篇文章,见下图

    ┌───────────────────────┐
    ┌─>│        timers         │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    │  │     I/O callbacks     │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    │  │     idle, prepare     │
    │  └──────────┬────────────┘      ┌───────────────┐
    │  ┌──────────┴────────────┐      │   incoming:   │
    │  │         poll          │<─────┤  connections, │
    │  └──────────┬────────────┘      │   data, etc.  │
    │  ┌──────────┴────────────┐      └───────────────┘
    │  │        check          │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    └──┤    close callbacks    │
    └───────────────────────┘

    可以看出来在同一次event loop内,timers阶段是在check阶段之前的,也就是说若是在同一个event loop内既有定时器到期又有setImmediate那肯定是setTimeout先执行。若不在一个event loop内那就不好说了,要具体情况具体分析了。 那么setTimeout(n) 和 setImmediate的执行顺序到底是啥?

    • 当setTimeout与setImmediate在同一时间执行且是在event loop之前执行的设置代码, 又且 n <= t(t是当执行event loop时setTimeout刚好过期的那个时间) 此时就相当于 在第一次event loop内既有setTimeout 过期又有 setImmediate,显然 setTimeout先执行。
      
      // test.js
      setTimeout(() => {
      console.log('setTimeout');
      }, 1)
      setImmediate(() => {
      console.log('setImmediate');
      });

    // 假设我的t 是2,那么输出就是

    setTimeout

    setImmediate

    * 当setTimeout与setImmediate在同一时间执行且是在event loop之前执行的设置代码, 又且 n > t(t是当执行event loop时setTimeout刚好过期的那个时间)
    此时就相当于 在第一次event loop内只有 setImmediate,setTimeout的过期只有在接下来的event loop内被检查到了显然 setImmediate先执行。
    代码同上,把n 换成 大于2的时间。
    > t的抉择就完全取决于机器的性能了
    而且在此处还有一个问题就是node中setTimeout最小时间1毫秒,见源码如下

    //lib/internal/timers.js~line#34 after *= 1; // coalesce to number or NaN if (!(after >= 1 && after <= TIMEOUT_MAX)) { if (after > TIMEOUT_MAX) { process.emitWarning(${after} does not fit into + ' a 32-bit signed integer.' + '\nTimeout duration was set to 1.', 'TimeoutOverflowWarning'); } after = 1; // schedule on next tick, follows browser behavior }

    也就是说你设置0,最下也是1,那么就是说若你的机器性能很好,在1毫秒之前就开始执行event loop了,那么将会永远只看到setImmediate在setTimeout之前执行了。
    * 前面说的都是在event loop之前设置,那么在event loop的期间执行js代码时设置的呢(我们只说同时设置的执行顺序)?(参见我前面的关于js代码的执行时机解释)
    * timers阶段设置

    setTimeout(() => { // 1 console.log('外层timeout'); setTimeout(() => { // 2 console.log('set timeout in timeout'); }); setImmediate(() => { // 3 console.log('set immediate in timeout'); }); });

    1的回调是在timers阶段执行的,而setTimeout内部的定时器一定是不可能在本次event loop的timers阶段执行的(分析后面再说),那就是说2的回调一定是在本次event loop之后的某次loop中的timers阶段执行的,可我们也要注意,本次loop的check阶段还没执行,因此就很明显了,3的回调会在本次loop的check阶段执行,因此得到总结**在timer阶段设置的setTimeout和setImmediate一定是setImmediate先执行**。
    * I/O callbacks, idle, prepare, poll, 同理这些阶段也一定是setImmediate先执行,我们最常处于的阶段可能就是poll阶段了,因为这是描述符事件回调的触发阶段,在这个阶段,本轮loop的timers阶段已经执行过了,所以setTimeout,一定是后面的loop timers阶段执行,而本轮loop的check阶段还没有执行,因此在这些阶段设置的setImmediate可以在本轮loop得到执行,所以**在timer阶段之后,check阶段之前设置的setTimeout和setImmediate一定是setImmediate先执行**。

    const fs = require('fs');

    fs.readFile('./test.js', () => { setTimeout(() => { console.log('set timeout in poll phase'); }); setImmediate(() => { console.log('set immediate in poll phase'); }); });

    * check阶段,在本轮设置的setTimeout和setImmediate执行顺序与在event loop执行之前设置的是一样的,执行顺序不定取决于设置的超时时间t(原因同在event loop执行之前设置的是一样,甚至可以说两者就是可以看做是等同的)。因为在这一阶段设置的setImmediate是不会在本轮check阶段执行的,同理setTimeout也是没办法在本轮执行的。
    
    总的可以用一个流程图来概括
    {% asset_img timerheimmediate.png timer & check phase %}
    
    ### 接下来 process.nextTick VS setImmediate
    事实上这两者是没有可比的
    1. process.nextTick的实现是基于v8 MicroTask(是在当前js call stack 中没有可执行代码才会执行的队列,低于js call stack 代码,但高于事件循环,浏览器中也是如此[可参见](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/))机制的。不属于event loop(v8 microtask是怎么和node结合起来的还没看明白失败啊)
    但是可以确定是nextTick走的是v8的mircotasks机制,且在当前js calk stack 结束后event loop 继续进行之前调用,也就是说要是写一个递归nextTick调用会把整个node阻塞掉

    setImmediate(() => { console.log('immediate'); }); function a() { process.nextTick(() => { console.log('set nextTick'); a(); }); } a();

    可以试一下上面的代码, 你会发现这就是无限调用了。还有我们所熟知的promise也走的是v8 microtask机制(并且Promise使用时v8实现的promise)。那么同理,promise的then的回调和nextTick中的回调也是由v8 microtask机制来确定执行的,也是与event loop 无关的。当然这在promiseA+规范里面也是有相应描述的。为了测试我们可以执行以下代码

    const promise = Promise.resolve(234)

    setImmediate(() => { console.log('immediate'); });

    testPromise();

    function testPromise() { promise = promise.then(() => { console.log('promise'); testPromise(); }); }

    
    是的结果和执行pross.nextTick是一样的event loop被阻塞了。
    
    在源代码里我们也可以看到见下
    主要代码在/lib/internal/procss/next_tick.js(这里有太多的逻辑没搞明白只是知道了一个大概)
    
    {% asset_img tickQueue.png tick queue %}
    
    我们所创建的nextTick都是由这个全局的NextTickQueue来管理的,当我们执行nextTick,就push进去一个TickObject
    
    {% asset_img tickObject.png tick object %}
    
    执行nextTick的逻辑如下
    {% asset_img nextTick.png nextTick %}
    
    接下来就是触发nextTickQueue里面的tickObject的执行了
    {% asset_img tickCb.png tick queue handle %}
    
    在接下来就是设置_tickCallback(_tickDomainCallback是使用了Domain的版本)这个回调的执行时机了
    {% asset_img setUpNextTick.png setUpNextTick %}
    
    失败的地方就来了,我跟到C++代码里面后就完全没找到_tickCallback的具体执行时机的设置了,而且这里面也有太多的逻辑了,完全不知道是在干啥,还有待慢慢揭秘求高手。
    
    2. setImmediate是基于libuv的event loop的。
    
    到了这里我可以知道了nextTick一定是先执行的(同时设置)

总结一下

  1. 当同时设置nextTick, setImmediate, setTimeout时一定是nextTick先执行,nextTick不属于event loop属于v8的micro tasks
  2. setImmediate, setTimeout是属于event loop的,但是执行的阶段不同。
  3. nextTick的promise的回调执行是在event loop继续执行之前的,也就是说他们的调用是会阻塞event loop的。也就是说在使用nextTick和promise编写递归调用或者大循环时要小心阻塞event loop
  4. 在setImmediate, setTimeout设置再次设置自己时,一定不会再本次loop中执行的
  5. 在浏览器中我猜测setTimeout和promise, MicroTasks也是这样的

最后再来点儿,对于setTimeout和setImmediate的代码分析,来具体解释为什么4成立