node端事件循环机制(Part1)

30次阅读

node 是基于谷歌 v8 javascript 引擎的非阻塞、事件驱动平台, 接下来的一系列文章,我将描述什么是事件循环,它是如何工作的,它如何影响我们的应用程序。
文章指引

Event Loop (本文)
Timers、Immediates、Next Ticks
Promises、Next-Ticks、Immediates
处理 I/O
最佳的事件循环练习
在 Node v11 中 timers、microtasks 发生的改变

Reactor 模式

nodejs 的事件驱动模型涉及 Event Demultiplexer 和 Event Queue。所有 I / O 请求最终将生成完成 / 失败事件或任何其他触发器,统称为事件。这些事件按照以下算法处理。

Event Demultiplexer 接收 I / O 请求,并将这些请求委托给相应的硬件。
一旦 I / O 请求被处理 (文件中的数据、套接字中的数据可以读取等),Event Demultiplexer 将为这个特定的操作注册回掉并添加到 Event Queue 中
当 Event Queue 中的事件可以被处理时,它们将按照接收到的顺序顺序执行,直到队列为空。
如果 Event Queue 中没有事件,或者 Event Demultiplexer 中没有任何挂起的请求,则程序将完成。否则,这个过程将又从第一步开始,如此循环。

注意: 不要混淆 event loop 和 NodeJS Event Emitter。NodeJS Event Emitter 与此机制完全不同。在后面的文章中,我将通过 event loop 解释 NodeJS Event Emitter 如何影响事件处理过程。
上面的图是对 NodeJS 如何工作的高级概述,并显示了一个称为 Reactor 模式的主要部分。但实际却这比这复杂得多。这有多复杂呢?

Event Demultiplexer 不是一个独立的部分,它讲包括所有操作系统平台以及所有类型的 I /O。这里显示的 Event Queue 也仅仅包含一个的队列,所有类型的事件都在其中排队进入和退出队列。并不是只有 I / O 类型的事件会在这里排队

所以接下来让我们更加深入的理解
Event Demultiplexer
Event Demultiplexer 只是一个抽象概念,它并不真实存在。在不同的操作系统中,它都有实现,有着不同的名称。
如 Linux 中称它 epoll、BSD 系统中称 kqueue、Solaris 中称 event ports、Windows 中的 IOCP 等。Nodejs 使用它提供的非阻塞、异步硬件 I / O 功能。
File I/ O 的复杂性
令人困惑的是,并不是所有的 I / O 类型都能使用这个实现来执行,即使在同一个操作系统平台上,支持不同类型的 I / O 也很复杂。
例如 Linux 不支持文件系统访问的完全异步,为了提供完全的异步,要处理所有这些文件系统的复杂性是非常复杂的 / 几乎不可能的。
另外处理 File I/ O 之外,node 提供的 DNS 也具有这种复杂性
解决办法
因此,引入了 thread pool (线程池) 来支持 I / O 函数,而硬件异步 I /O utils(如 epoll/kqueue/event ports 或 IOCP) 无法直接处理这些函数。
现在我们知道并不是所有的 I / O 函数都发生在 thread pool 中。NodeJS 已经尽力使用非阻塞和异步硬件 I / O 来完成大部分 I /O,但是对于阻塞或难以处理的 I / O 类型,它使用 thread pool。
将前面提到的问题聚集到一起
正如我们所看到的,很难在所有不同类型的 OS 平台上支持所有不同类型的 I /O((file I/O, network I/O, DNS 等)。一些 I / O 可以使用本地硬件实现来执行,同时保留完整的异步性。而一些特定的 I / O 应该在线程池中执行某些 I / O 类型,以保证完整的异步性。
开发人员对 Node 的一个常见误解是 Node 在线程池中的执行所有类型的 I /O。
为了在支持跨平台 I / O 的同时管理整个流程,应该有一个抽象层来封装这些平台间和平台内的复杂性,并为 node 的上层公开一个通用 API。
接下来就让我们热烈欢迎 ……..

libuv 是一个跨平台支持库,最初是为 NodeJS 编写的。它是围绕事件驱动的异步 I / O 模型设计的。这个库提供的不仅仅是对不同 I / O 轮询机制的简单抽象:‘handles’和‘streams’为套接字和其他实体提供了高级抽象; 此外,还提供了跨平台的文件 I / O 和线程功能。

现在我们来看看 libuv 是如何组成的

从图中我们可以看到 Event Demultiplexer 是 Libuv 抽象的 I / O 处理 api 集合,并公开给 NodeJS 的上层。libuv 它不仅仅为 Node 提供 Event Demultiplexer。Libuv 为 NodeJS 提供了整个事件循环功能,包括 Event Queue
接下来让我们来看 Event Queue
Event Queue
事件队列中,所有事件都被事件循环按顺序排队和处理,直到队列为空。
在 NodeJS 中有多个队列,不同类型的事件在它们自己的队列中排队。在处理一个类型的队列之后,在进入下一个队列之前,事件循环将处理两个 intermediate queues(中间队列),直到中间队列为空

那么有多少个队列呢? 什么是中间队列?
有四种主要的事件类型,它由本地的 libuv 处理。

过期的 timers 和 intervals 队列: 由使用 setTimeout、setInterval 增加的 callback 组成.
I/ O 事件队列: 由已完成的 I / O 事件组成.
Immediates 事件队列: 由通过 setImmediate 增加的 callbacks 组成
Close 事件队列: 由 close event handlers 组成

有两种类型的 intermediate queues(中间队列),他们由 node 处理, 不属于 libuv

Next Ticks 队列: 由使用 process.nextTick 添加的事件组成
其他的 Microtasks Queue (微队列): 包含了其他的微任务,例如 Promise.then()

这些不同类型的事件队列如何工作?
如下图所示,Node 通过检查计时器队列中的是否有过期计时器从而开始事件循环,并在每个步骤中遍历每个队列。在处理了 close handlers queue 之后,如果任何队列中没有要处理的项,则循环将退出。可以将事件循环中每个队列的处理视为事件循环的一个阶段。

前面提到的中间队就是图中心得两份队列,有趣的是,一旦一个阶段完成,事件循环将检查这两个中间队列中的任何可用项。如果中间队列中有任何可用项,事件循环将立即开始处理它们,直到清空两个立即队列。直到它们为空,事件循环才会继续到下一个阶段。
Next tick queue vs Other Microtasks queue
两个中间队列的优先级不同,Next tick 队列具有比其他微任务队列更高的优先级。也就是说当一个阶段完成之后,会先去 Next tick 队列清空任务,再去 Microtasks(微任务) 队列清空任务。
这些所谓的中间队列的约定引入了一个新问题,IO 饥饿。大量使用 process.nextTick 将强制事件循环在不向前移动的情况下无限期地处理 next tick 队列。这将导致 IO 饥饿,因为如果不清空 Next tick 队列,事件循环将无法继续。
我将在后面的文章中以示例深入描述这些队列。
来一个总结,node 在 Event Demultiplexer 中处理所有的异步 I /O,它是 Libuv 抽象的 I / O 处理 api 集合, 在 I / O 响应后,将相应的事件推入到相应的事件类型的队列,并在其中通过事件循环来调用回掉函数。
最后,现在您知道了什么是事件循环 (即在 event)、如何实现它以及 Node 如何处理异步 I /O。现在让我们看看 Libuv 在 NodeJS 体系结构中的位置。

Reference

https://jsblog.insiderattack…. (自备梯子)

正文完
 0