nodejs event-loop

563 阅读4分钟

同步任务和异步任务

   // test.js
   setTimeout(() => console.log(1));
   setImmediate(() => console.log(2));
   process.nextTick(() => console.log(3));
   Promise.resolve().then(() => console.log(4));
   (() => console.log(5))();
      
   // 5
   // 3
   // 4
   // 1
   // 2

上边这段代码中 (() => console.log(5))(); 为同步任务,所以最先执行,剩下的为异步任务,

其中

     process.nextTick(() => console.log(3));
     Promise.resolve().then(() => console.log(4));

总是先于定时器执行,可以理解为 **process.nextTick和promise在下一轮事件循环之前执行,而执行到定时器的时候会计算时间阀阈值,达到时间阈值之后,添加入事件循环队列,**所以 总是3,4 早于 1,2执行

总之 process.nextTick、promise是最先执行的异步任务

其次,process.nextTick早于promise先执行,Node 执行完所有同步任务,接下来就会执行process.nextTick的任务队列,promise任务追加在process.nextTick队列之后

所以以下代码总会先输出 3,再输出 4

      process.nextTick(() => console.log(3));
      Promise.resolve().then(() => console.log(4));
      // 3
      // 4

需要注意的是,只有上一个队列清空完毕之后,才会执行下一个队列

    process.nextTick(() => console.log(1));
    Promise.resolve().then(() => console.log(2));
    process.nextTick(() => console.log(3));
    Promise.resolve().then(() => console.log(4));
    // 1
    // 3
    // 2
    // 4

什么是event loop

js是单线程的,事件循环使nodejs将非阻塞的I/O操作转移到系统内核来执行(windows平台下的 IOCP, *nix为自定义的线程池),它们可以在后台执行多个操作,当其中一个操作完成的时候,通知nodejs,以便将回调添加到轮询队列以最终执行

当nodejs开始执行,处理输入的脚本,该脚本可能进行异步 api 调用、调度定时器或者调用 process.nextTock(),然后开始处理时间循环

下图展示了事件循环操作顺序的简化概览:

每个框都被称为事件循环机制的一个阶段,每个阶段都有一个先进先出的(FIFO)的回调队列,通常情况下,当事件循环进入给定的阶段,它将在该阶段执行队列中的回调,直到队列耗尽或者达到执行回调的最大数目,当队列耗尽或者达到执行回调的最大数目的时候,事件循环将进入到下一个阶段,依次类推

阶段概述

timers:在这个阶段执行setTimeout、setInterval的回调

**pending callbacks:**执行延迟到下一个循环迭代的I/O回调

**idle, prepare:**仅内部使用

**poll:**这个阶段是轮询阶段,用于检索新的I/O事件,等待还未返回结果的 I/O 事件,执行与I/O相关的回调队列,如果没有其它的异步任务要处理(比如到期的定时器),则会一直停留在这个阶段,

这里的回调,除了以下回调

· setTimeout 和 setInterval的回调

· setImmediate 的回调

· 用于关闭请求的回调,如:socket.on('close',...)

**check:**该阶段执行 setImmediate 的回调

**close callbacks:**该阶段执行关闭回调,比如: socket.on('close',...)

阶段的详细介绍

timers

计时器指定了一个阈值,在这个阈值之后可以执行一个提供的回调函数,注意阈值不是指执行的确切时间,计时器回调将在指定的时间过后尽快运行,执行其它操作被阻塞时会延迟执行

    const fs = require('fs');
    
    function someAsyncOperation(callback) {
      // 假设这个读取文件的操作需要 95ms 才能完成
      fs.readFile('/path/to/file', callback);
    }
    
    const timeoutScheduled = Date.now();
    
    setTimeout(() => {
      const delay = Date.now() - timeoutScheduled;
    
      console.log(`${delay}ms have passed since I was scheduled`);
    }, 100);
    
 
    someAsyncOperation(() => {
      const startCallback = Date.now();
    
      // 做一些其它操作 需要 10ms
      while (Date.now() - startCallback < 10) {
        // do nothing
      }
    });

1. 当事件循环执行到轮询阶段时(在轮询阶段等待),此时它有一个空队列(fs.readFile尚未完成),

2. 在等待的第 95ms 的时候,读取文件完成,并且将需要执行10ms的回调添加到轮询的回调队列,并执行,

3. 回调完成之后,事件循环查看到已经达到阈值的计时器,然后返回到计时器阶段执行计时器回调

4. 在此示例中将看到计划计时器,与执行计时器回调总为 105ms

pending callbacks

这个阶段会执行一些系统操作的回调,如 TCP 错误的类型,例如,如果 TCP 的 socket在尝试连接时收到的 'ECONNREFUSED',一些 *nix 系统希望等待报告错误,这将会在 pending callbacks 阶段的回调队列执行

poll

这个阶段主要做两件事情

1. 计算它应该阻塞多长时间,并轮询 I/O

2. 处理轮询队列中的回调

当事件循环进入 poll 阶段,并没有计时器任务时,将发生以下这两种情况之一:

1. 如果轮询回调队列不是空的,则事件循环将依次同步执行它们,

2. 如果轮询回调队列是空的,则将发生另外两种情况之一:

1. 如果安排了 setImmediate,则事件循环将结束轮询阶段,并继续进入check阶段,执行回调队列

2. 如果没有安排 setImmediate,则事件循环将继续等待回调脚本添加到当前阶段的队列

此外,只要轮询队列为空,事件循环将检查已经到达阈值的计时器,如果一个或者多个计时器准备好了,将回滚到计时器阶段来执行这些计时器回调队列

check

通常代码执行时,事件循环最终将达到轮询阶段,在轮询阶段它将等待传入的连接、请求等I/O事件,但是如果已经使用setImmediate安排了回调,并且轮训阶段回调队列为空,则它将进入检查阶段,而不是继续等待轮训事件,

close callbacks

如果一个 socket 或者 handle 突然关闭(例如:socket.destory()),则将在此阶段触发close事件

setImmediate和setTimeout

由于setTimeout 在timers 阶段执行,setImmediate 是在 check 阶段执行的,理论上来讲当 setTimeout 第二个参数即阈值设置为 0 的时候,setTimeout 会比 setImmdiate 先执行,即

      setTimeout(() => console.log(1), 0);
      setImmediate(() => console.log(2));

上述代码执行顺序为 1 , 2,但是 实际情况是不确定的,因为nodejs 是做不到 0ms的,当第二个参数大于2147483647或小于1时,将自动设置为1,非整数的将自动截取为整数

实际执行时,进入事件循环的时候如果到了 1ms,则会先执行 setTimeout,如果没有到 1ms 则会跳过 timers 阶段,进入 check 阶段,先执行 setImmediate,所以这受到当前进程的性能影响,

但是如果在 I/0 回调内调用这两个任务,则setImmediate 一定先于 setTimeout 执行

    const fs = require('fs');
    
    fs.readFile('test.js', () => {
      setTimeout(() => console.log(1));
      setImmediate(() => console.log(2));
    });

上述代码会先进入 poll 阶段,然后进入 check 阶段,最后再进入 timers阶段

参考链接:

nodejs.org/en/docs/gui…

www.ruanyifeng.com/blog/2018/0…