举个栗子

好读书,不求甚解

对Node.js eventloop的理解

Kevalin's Avatar 2017-03-14

Node.js的结构

对Node.js的理解

  • V8 engine是将javascript代码编程成机器语言给计算机执行,类似于一个编译器。
  • event loop和异步I/O是使用libuv来实现(libuv是用来处理跨平台的event loop和异步I/O的)。对于不同操作系统在异步I/O的处理上存在一点区别,以Linux系统为例,network类异步I/O是用epoll解决,file类异步I/O是采用自建线程池解决(默认4个线程);Windows是采用IOCP,内部也是线程池来处理异步I/O。

Node.js主程序只运行在一个进程上,执行一个一个任务,如果有异步I/O操作,就分配给线程池处理,然后继续执行主进程下面的任务,异步I/O执行完毕之后,又回到主进程执行对应的回调。

libuv有的功能:

  • Full-featured event loop backed by epoll, kqueue, IOCP, event ports.
  • Asynchronous TCP and UDP sockets
  • Asynchronous DNS resolution
  • Asynchronous file and file system operations
  • File system events
  • ANSI escape code controlled TTY
  • IPC with socket sharing, using Unix domain sockets or named pipes (Windows)
  • Child processes
  • Thread pool
  • Signal handling
  • High resolution clock
  • Threading and synchronization primitives

问题:因为是进程池进行处理,所以会不会出现进程池被占满的情况呢?如果出现这样的情况,Node.js会发生什么?

对event loop的理解

官方介绍上的图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

这里涉及了所有event loop中存在的情况,官方解释:

  • timers: this phase executes callbacks scheduled by setTimeout() and setInterval().
  • I/O callbacks: executes almost all callbacks with the exception of close callbacks, the ones scheduled by timers, and setImmediate().
  • idle, prepare: only used internally.
  • poll: retrieve new I/O events; node will block here when appropriate.
  • check: setImmediate() callbacks are invoked here.
  • close callbacks: e.g. socket.on(‘close’, …).

似乎不怎么好理解(年纪大了,个人理解能力有限),还好有另一种解释,将各种情况分成了2类:

microtasks:

  • process.nextTick
  • promise
  • Object.observe

macrotasks/tasks:

  • setTimeout
  • setInterval
  • setImmediate
  • I/O

它们之间有执行先后顺序。在一个cycle中,当stack空了之后,先执行microtasks,miscrotasks空了之后再执行macrotasks。

有个更清晰的图:

结合上面的图来解读一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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')
})

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
script start  
promise1
promise2
setInterval
setTimeout1
promise3
promise4
setInterval
setTimeout2
setInterval
promise5
promise6

问题:在官方介绍中提到setTimeout(fn, 0)和setImmediate(fn)这2个在一个cycle中的时候,一定是setImmediate要先执行,但在实际测试中发现不一定,囧(/ □ ),难道我写的代码有问题,还是Node版本太旧?

个人理解不代表官方,理解有问题的地方欢迎大家随时联系指出。

参考文档

本文作者 : Kevalin
本文使用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议
本文链接 : https://kevalin.github.io/2017/03/14/%E5%AF%B9Node-js-eventloop%E7%9A%84%E8%AE%A4%E8%AF%86/