You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
正如你下面看到的这个图,Node 从检查定时器队列中有没有到期的定时器回调函数开始,随后在每个步骤中检查其他所有队列,并维护一个引用计数器来记录需要被处理的项目总数。在处理完 close 事件队列之后,如果在所有队列中都没有需要被处理的项目,那么事件循环将会退出。事件循环中的每个队列的处理可以看作是事件循环的一个阶段。
例如,事件循环目前在处理 immediates 队列,队列中有 5 个回调函数需要被执行,同时,有两个函数被添加到了 next tick 队列中,当 immediates 队列的 5 个函数被执行后,事件循环在移动到 close handlers 队列之前,会立刻检测到在 next tick 队列中有待处理的程序,事件循环会执行 next tick 队列的所有回调,然后才继续处理 close handlers 队列的回调。
这些所谓的中间队列机制带来了新的问题,IO 饿死问题。不断地使用 process.nextTick 函数将回调函数添加到 next tick 队列里面将会逼迫事件循环不断地处理 next tick 队列中的函数而迟迟不进入下一个阶段。这样会引起 IO 饿死问题因为 next tick 队列始终不为空导致事件循环停留在某一阶段。
为了解决这个问题,我们可以通过设置 process.maxTickDepth 这个参数来限制 next tick 队列中回调函数的最大数量。但是这个参数已经因为某些原因在 NodeJs v0.12 之后被移除了。
原文链接:https://jsblog.insiderattack.net/event-loop-and-the-big-picture-nodejs-event-loop-part-1-1cb67a182810
NodeJS 与其他语言编程平台就是它处理 I/O 的方式。我们一直以来听到关于 NodeJS 的介绍就是“基于谷歌 v8 javascript 引擎的非阻塞的,事件驱动的运行平台“。这是什么意思呢?什么是 “非阻塞” 和 “事件驱动” ?而这些问题的答案,尽在 NodeJS 的事件循环的核心中。在本系列文章中,我将会解释事件循环是什么,它是如何工作的,以及它是怎么在我们的应用程序中产生影响的和如何充分利用它等等。关于此文章,为什么选择编写系列文章而不是集中到一篇文章里面?这是因为集中到一篇文章中使文章篇幅太长进而导致我会忽略某些知识点,因此我打算编写成一个系列的文章。在第一篇文章中,我将会解释 NodeJS 是如何工作的,它是如何访问底层 I/O 的和它如何与其他不同的平台进行协作等等。
文章系列目录
Reactor 模式
NodeJS 以事件驱动的方式运行,而事件驱动包含了 Event Demultiplexer(事件多路分发器)和Event Queue(事件队列)两个部分。所有的 I/O 请求最终都会生成一个成功或者失败的事件,或者是其他类型的触发器,而这些统称为事件。这些事件都会遵循以下的算法来进行处理自身逻辑。
协调编排以上整个处理过程的程序就是事件循环。
事件循环是一个单线程与半无限循环的机制,之所以叫做半无限循环是因为事件循环在遇到没有任务需要执行的时候,会退出循环。在开发人员的视角来看,这就是程序退出的地方。
上面这个图是对 NodeJS 工作原理的抽象概述,并展示了 Reactor Pattern 设计模式的主要部分。但是实际机制比概述要复杂得多,那有多复杂呢?
让我们一起来深入研究。
Event Demultiplexer
Event Demultiplexer(事件分发器)并不是一个真实存在的组件,而是一个 reactor 模式中一个抽象的概念。在现实中,event demultiplexer(事件分发器)在不同的操作系统中有着不同的实现并有不一样的名字例如在 Linux 系统中是 epoll, 在 BSD 系统(MacOS)中是 kqueue, 在 Solaris 系统中是 event ports, 在 Windows 系统中是 IOCP(Input Output Completion Port) 等等。NodeJS 则是利用了这些底层硬件实现提供的非阻塞异步的 I/O 能力。
Complexities in File I/O
但是令人困惑的是,并不是所有的 I/O 类型可以使用上面这些底层实现来执行。甚至在同一个操作系统中,支持不同类型的 I/O 类型操作都是非常复杂的。通常来说,网络 I/O 可以使用类似 epoll, kqueue, event port 和 IOCP 这些非阻塞方式来实现,但是文件 I/O 会更加复杂。在某些系统中,例如 Linux 系统不支持完全异步访问文件系统。并且在 MacOS 系统的文件系统中使用 kqueue 来实现事件的通知/信号也有诸多限制(你可以在这篇文章中阅读其复杂性)。因此兼容不同的操作系统的文件系统以提供完全异步的文件访问能力是非常复杂的。
Complexities in DNS
和文件 I/O 类似,Node API 提供的某些 DNS 函数也有某些兼容性问题。因为 NodeJS DNS 函数例如 dns.lookup 需要访问系统的配置文件例如 nsswitch.conf, resolve.conf 和 /etc/hosts ,上面所说的文件系统的复杂性也同样体现在 dns.resolve 上。
The solution ?
因此,为了提供访问 I/O 的能力而又不能直接使用底层异步 I/O 函数例如 epoll/kqueue/event port/IOCP 来实现的情况下,我们需要引入线程池。现在我们知道不是所有的 I/O 函数都发生在线程池中。NodeJS 充分利用了底层的异步非阻塞 I/O 函数,但是对于那些阻塞的或处理逻辑复杂的 I/O 类型将会使用线程池来实现。
Gathering All Together
正如我们看到的,在现实中很难基于所有不同类型的操作系统来支持所有不同类型的 I/O(文件 I/O, 网络 I/O, DNS 等等)。一些 I/O 可以使用原生的底层函数来实现,并保持完全异步,而某些 I/O 类型为了确保函数的异步性使用了线程池来实现。
为了处理整个流程并支持跨平台 I/O,我们需要一个抽象层来封装各个平台间的与平台内部的复杂操作,并暴露一些通用的 API 供 Node 上层进行调用。
谁比较适合做这些工作呢?各位,欢迎...
从 libuv 的官方文档来看,
现在我们一起看一下 libuv 的组成部分。下面这个图是来自 libuv 的官网文档的,它描述了 libuv 是如何处理不同类型的 I/O 并暴露出一个通用的 API 的。
现在我们知道 Event Demultiplexer(事件分发器)并不是一个单个实体,而是一个利用 libuv 封装处理 I/O 来给上层调用的一个 API 集合。
libuv 对于 Node 来说它不仅是一个 event demultiplexer(事件分发器),它还提供了包括事件队列排队机制的整个事件循环的核心功能。
我们现在一起看一下事件队列。
Event Queue
事件循环被认为是一种数据结构,它能够让所有事件在其中排队和被顺序处理直到整个队列为空。但是实际在 Node 中情况和 reactor 模式中描述的完全不一样,有什么不同呢?
所以到底有多少个队列呢?而所谓的中间队列又是什么呢?
原生的 libuv 事件循环队列会有 4 种主要类型。
除了这四种主要类型的队列,还有两种就是上面我所说被 Node 处理的 “中间队列”。虽然它们不是 libuv 本身的一部分,但是却是 NodeJS 的一部分,它们就是,
How does it work?
正如你下面看到的这个图,Node 从检查定时器队列中有没有到期的定时器回调函数开始,随后在每个步骤中检查其他所有队列,并维护一个引用计数器来记录需要被处理的项目总数。在处理完 close 事件队列之后,如果在所有队列中都没有需要被处理的项目,那么事件循环将会退出。事件循环中的每个队列的处理可以看作是事件循环的一个阶段。
对于使用红色标红的中间队列来说,有趣的在于,只要一个阶段完成之后,事件循环会去检查那两个中间队列是否有可执行的项目。如果有,那么事件循环将会立刻开始处理这两个中间队列的项目直到队列为空。当它们为空,事件循环才会继续下一个阶段的处理。
注:这里译者按照自己的理解,提供一个 Demo 代码:
Next tick queue vs Other Microtasks
Next tick 队列比其他 micro tasks 队列有更高的优先级。不过,它们都是在事件循环的两个阶段之间被处理,libuv 会在每个事件阶段结束后将通信回传给 Node 上层抽象。你会注意到上面的图中已被我使用深红色来标注 next tick 队列,这意味着 next tick 队列中的函数会比 resolved promise 优先处理。
这些所谓的中间队列机制带来了新的问题,IO 饿死问题。不断地使用 process.nextTick 函数将回调函数添加到 next tick 队列里面将会逼迫事件循环不断地处理 next tick 队列中的函数而迟迟不进入下一个阶段。这样会引起 IO 饿死问题因为 next tick 队列始终不为空导致事件循环停留在某一阶段。
我在后面的文章中将会深入讲解每个队列的知识点。
最后,现在你已经知道了什么是事件循环,它是如何实现的,并且 Node 是如何处理异步 I/O 的。一起来看一下 Libuv 在 NodeJs 的结构中处于什么位置。
我希望你能够在读完本文章后有所收获,后面的文章,我将会讲解:
和关于它们的更多细节。如有错误或需添加的内容请随时在评论区添加你的见解。
References:
NodeJS API 文档 https://nodejs.org/api
NodeJS Github https://github.com/nodejs/node/
Libuv Official Documentation http://docs.libuv.org/
NodeJS Design Patterns https://www.packtpub.com/mapt/book/web-development/9781783287314
Everything You Need to Know About Node.js Event Loop — Bert Belder, IBM https://www.youtube.com/watch?v=PNa9OMajw9w
Node’s Event Loop From the Inside Out by Sam Roberts, IBM https://www.youtube.com/watch?v=P9csgxBgaZ8
asynchronous disk I/O http://blog.libtorrent.org/2012/10/asynchronous-disk-io/
Event loop in JavaScript https://acemood.github.io/2016/02/01/event-loop-in-javascript/
The text was updated successfully, but these errors were encountered: