NodeJs 事件循环机制

Posted by Rimin on 2019-05-01

上一篇 主要是Js的事件机制,但是 Node 的时间循环机制和JS本身并不相同。

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.
(NODE的事件循环就是允许Node执行非阻塞I/O操作,尽管js本身是单线程的,但是通过尽可能地将操作卸载至核心系统)
——node 官方文档

由于我们知道Javascript是单线程的,所以按常识很容易理解为什么它不能充分利用多核CPU。事实上,在Node中Javascript是单线程的之外,Node自身其实是多线程的,只是I/O线程使用的CPU较少。另外,除了用户代码无法并行执行外,所有的I/O(磁盘I/O和网络I/O等)则是可以并行起来的。

Node.js 采用事件驱动和异步 I/O 的方式,实现了一个单线程、高并发的 JavaScript 运行时环境.除了 主线程 Node.js在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。

当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop)

node异步I/O

image

(图片来自《node深入浅出》朴灵)

这幅图大概就可以概括node的异步I/O运行机制。
但是这里node的I/O异步机制不作重点讨论,因为I/O的涉及到操作系统,node C++模块等较为底层的东西,这里暂不做讨论。

非I/O异步

在 node 中实现异步可以有

  • setTimeout和setInterval与浏览器中的API一致。
  • process.nextTick()
  • setImmediate()

先看这幅图

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 │
└───────────────────────┘

Node.js的单线程并不是真正的单线程,只是开启了单个线程进行业务处理(cpu的运算),同时开启了其他线程专门处理I/O。当一个指令到达主线程,主线程发现有I/O之后,直接把这个事件传给I/O线程,不会等待I/O结束后,再去处理下面的业务,而是拿到一个状态后立即往下走,这就是“单线程”、“异步I/O”。

各个阶段分析
  • timers: executes callbacks scheduled by setTimeout() and setInterval(). (这个阶段执行setTimeout()setInterval()设定的回调。一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们)。

  • pending callbacks: Executes I/O callbacks deferred to the next loop iteration。 This phase executes callbacks for some system operations such as types of TCP errors ( 执行延迟到下一个循环迭代的I / O回调, 注意是延迟的I/O回调, 这个阶段会执行一些错误的回调如TCP错误)

  • idle, prepare: only used internally(仅仅内部使用).

  • poll: Retrieve new I/O events; execute I/O related callbacks (almost all with the exception of close callbacks, the ones scheduled by timers, and setImmediate()); Node will block here when appropriate。(获取新的I/O事件,执行I/O相关事件除了close callbacks, timers的回调, 和setImmediate()的回调)
    这个阶段有两个函数:
    1. 计算它应该在这里阻塞多久以及对I/O进行轮询,然后
    2. 处理poll里的事件

          - 如果 `poll `队列不空,`event loop`会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限
          - 如果` poll `队列为空,则发生以下两件事之一:
              0 ) 如果代码已经被`setImmediate()`设定了回调, `event loop`将结束` poll 阶段`进入` check` 阶段来执行 `check 队列`(里的回调)
              1 )  如果代码没有被`setImmediate()`设定回调`,event loop`将阻塞在该阶段等待回调被加入 `poll` 队列,并立即执行。
      一旦 the poll queue 为空,poll 会检查是否有 timers 事件已到达时间,如果有,就会返回timers phase 去执行回调
    
  • check: 执行setImmediate()设定的回调。这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。

setImmediate() is actually a special timer that runs in a separate phase of the event loop. It uses a libuv API that schedules callbacks to execute after the poll phase has completed.(setImmediate实际上是一个运行在node特定阶段的特殊的定时器, 它使用libuv API, 且在poll阶段结束后触发)

  • close: 执行比如socket.on(‘close’, …)的回调。如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。

接下来是一个例子:

1
2
3
4
5
6
7
setTimeout(() => {
console.log('timeout');
}, 0);

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

以上这个例子的执行结果实际上并不确定:

1
2
3
4
5
6
7
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

但是当你像下面这样:

1
2
3
4
5
6
7
8
9
10
const fs = require('fs');

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});

就能确定运行结果为:

1
2
immediate
timeout
process.nextTick()

先看一个例子;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
process.nextTick(function () {
console.log('nextTick延迟执行1');
});
process.nextTick(function () {
console.log('nextTick延迟执行2');
});
// 加入两个setImmediate()的回调函数
setImmediate(function () {
console.log('setImmediate延迟执行1');
// 进入下次循环
process.nextTick(function () {
console.log('process.nextTick 插入');
});
});
setImmediate(function () {
console.log('setImmediate延迟执行2');
});
console.log('正常执行');
1
2
3
4
5
6
7
// node v0.10.13

nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
process.nextTick 插入
setImmediate延迟执行2
1
2
3
4
5
6
7
// node v6.x
正常执行
nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
setImmediate延迟执行2
process.nextTick 插入
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
32
33
34
35
36
37
38
39
console.log(1)

setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
setTimeout(function () {
console.log(9);
});

new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})

process.nextTick(() => {
console.log(6)
})

// 结果
1
7
6
8
2
4
9
3
5
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
32
33
34
35
36
37
38
39
console.log(1)

setTimeout(() => {
console.log(2)
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
process.nextTick(() => {
console.log(3)
})
})
setImmediate(function () {
console.log(9);
});

new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})

process.nextTick(() => {
console.log(6)
})

// 结果
1
7
6
8
2
4
3
5
9

实际上, process.nextTick()并没有出现在node事件循环的上述各个阶段中。

process.nextTick() is not technically part of the event loop.Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop. Looking back at our diagram, any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues. This can create some bad situations because it allows you to “starve” your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase. ( process.nextTick()并不属于事件循环的一部分, nextTickQueue会在现有操作被完成后执行, 回头看那个事件阶段循环图, 如果在任何阶段都去执行process.nextTick, 那么有可能造成其他I/O回调等被“饿死”)

那么什么时候要用 process.nextTick() 呢?

  • Allow users to handle errors, cleanup any then unneeded resources, or perhaps try the request again before the event loop continues.(允许用户优先处理erroes等)
  • At times it’s necessary to allow a callback to run after the call stack has unwound but before the event loop continues.(当必须要在当执行栈为空的时候立即去执行的回调时)

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
EventEmitter.call(this);

// use nextTick to emit the event once a handler is assigned
process.nextTick(() => {
this.emit('event');
});
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});