一般Web后端软件都没有很多的复杂计算,CPU很多时间都是在等待I/O操作完成或者等待网络应答消息,下图是CPU的一级缓存和二级缓存,硬盘,内存操作所花费的CPU cycles。

在本文中我们将说明如何理解Node.js事件轮询。

I/O即输入/输出,通常指数据在内部存储器和外部存储器或其他周边设备之间的输入和输出

不同硬件设备I/O操作所花费CPU cycles

Action              Cost (CPU cycles)
L1 Cache*                         3
L2 Cache*                        14
RAM*                            250
Disk                     41,000,000
Network                 240,000,000
Clock cycle 是计算机基本时间单位

举个网络延迟的示例

$ ping google.com
64 bytes from 172.217.16.174: icmp_seq=0 ttl=52 time=33.017 ms  
.....
64 bytes from 172.217.16.174: icmp_seq=7 ttl=52 time=27.846 ms  

假设在这个示例中网络大概延迟为44ms,CPU的速率为4GHz,如果CPU不等待网络消息的延迟的时间,176,000,000 CPU cycles大概等同可以完成多次CPU一,二级缓存以及内存的I/O操作。

Node.js怎么解决CPU等待I/O的问题

对与I/O的操作,都是使用异步的方式,不用等待I/O操作完成也可以继续执行,那么怎么做到异步执行,那就是事件轮询(Event Loop)

什么是Event Loop

Event Loop 是一个很重要的概念,指的是计算机系统的一种运行机制.下图Node.js事件轮询模型

taskQueue队列中蓝色的是MicrotaskQueue,taskQueue中包含MicrotaskQueue

那么它是怎么运作的,跟着下面的代码来理解Event Loop

console.log('script start')

const interval = setInterval(() => {  
  console.log('setInterval')
}, 0)

setTimeout(() => {  
  console.log('setTimeout 1')
  Promise.resolve().then(() => {
    console.log('promise 3')
  }).then(() => {
    console.log('promise 4')
  }).then(() => {
    setTimeout(() => {
      console.log('setTimeout 2')
      Promise.resolve().then(() => {
        console.log('promise 5')
      }).then(() => {
        console.log('promise 6')
      }).then(() => {
        clearInterval(interval)
      })
    }, 0)
  })
}, 0)

Promise.resolve().then(() => {  
  console.log('promise 1')
}).then(() => {
  console.log('promise 2')
})

将会输出

script start  
promise1  
promise2  
setInterval  
setTimeout1  
promise3  
promise4  
setInterval  
setTimeout2  
setInterval  
promise5  
promise6  

第一个周期

setInterval,setTimeout 1任务被加入taskQueue,Promise.resolve 1和then被加入到microtaskQueue中,此时stack是空的,根据whatwg规范,在同一个周期内,应该先处理好microtaskQueue,Promise.resolve 1任务被执行

Task queue: setInterval, setTimeout 1

script start  
promise1  
promise2  

第二个周期

microtask已经被处理完, setInteval任务推入到stack中被执行,其它的setInterval任务加入到TaskQueue队列中,排在setTimeout1任务后面

Task queue: setTimeout 1, setInterval

script start  
promise1  
promise2  
setInterval  

第三个周期

setTimeout1任务推入到stack中被执行,promise 3promise 4 加入 microtasksQueue,据whatwg规范promise 3promise 4也会在同一周期被处理,setInterval任务和setTimeout2会被加入taskQueue队列

Task queue: setInterval, setTimeout 2

script start  
promise1  
promise2  
setInterval  
setTimeout1  
promise3  
promise4  

第四个周期

setInterval任务推入到stack中被执行,其它的setInterval任务加入taskQueue队列,在setTimeout 2的后面

Task queue: setTimeout 2, setInteval

script start  
promise1  
promise2  
setInterval  
setTimeout1  
promise3  
promise4  
setInterval  

第五个周期

setTimeout 2任务推入到stack中被执行, promise 5 and promise 6加入到microtask队列并被执行

script start  
promise1  
promise2  
setInterval  
setTimeout1  
promise3  
promise4  
setInterval  
setTimeout2  
setInterval  
promise5  
promise6  
参考链接risingstack,注意各种浏览器之间会有所差异jakearchibald

Node.js内部实现

  • V8

Google V8 javascript引擎,执行javascript代码

  • Libeio

Libeio是一个包含全部特性(read, write, open, close, stat, unlink, fdatasync, mknod, readdir)负责I/O异步操作库

也就是事件模型图中的后台线程部分
  • Libev

高性能的事件轮询模型,负责Microtasks和Macrotasks队列的轮询

事件模型中除开后台线程部分,其余都由Libev负责

参考链接