深入解析 Node.js 事件循环工作机制

阅读数:1177 2019 年 8 月 6 日

本文从对线程、事件循环、事件循环常见的问题和错误上分别进行说明,进一步探索了 Node 的核心工作原理。

每当人们谈论 Node.js 时,都会出现很多问题,比如它究竟是什么、这项技术有什么用、它是否有未来等等。

让我们尝试讨论第一部分。回答这个问题最简单的方法是列出 Node 在技术上的许多定义,如:

  • Node.js 是一个基于 Chrome V8 JavaScript 引擎构建的 Javascript 运行时环境。
  • Node.js 使用事件驱动的非阻塞 I/O 模型,这使其轻量且高效。
  • Node 包生态系统 (npm) 是全世界最大的开源库生态系统。

但是,这些答案并不能令我完全满意,因为缺少有些东西。在阅读上面的要点后,你可能会认为 Node.js 只是另一种 JavaScript 技术,但理解它最重要的方法是分析它是如何实现异步并具有完全非阻塞 I/O 系统的。

这才是为什么它能成为每个 Web 开发人员必备之物的真正原因。

准确了解 Node 如何在幕后工作不仅能增进对这项技术的更多了解,而且还会吸引那些还未用过它的人认识并开始学习它。

而对于那些已经是该领域专业人士的人来说,了解 Node 的内部和外部将使你成为一名最新和最前沿的开发人员,并且能够根据自身的需求提高 Node 的性能。

因此,为了挖掘 Node 的世界,我们将检查其核心的部分:事件循环,事实上,事件循环就是负责 Node 非阻塞 I/O 模型的部分。

对线程认识的简要刷新

在深入了解事件循环之前,我想花些时间在线程上。如果你想知道为什么这很必要,我会告诉你,为了更好地理解一个概念,我们必须首先开始在脑海中形成一个词汇表,它将有助于我们识别系统的每个部分。这样,在稍后阅读有关事件循环、事件循环如何工作以及线程的概念如何应用在事件循环中的内容时,你才会有很大的优势。

每当我们运行一个程序时,我们都会创建一个它的实例,并且我们会调用一些内部线程,他们是与该实例相关联的。线程可以看作是 CPU 必须执行的操作单元。许多不同的线程可以与程序的单个进程相关联。以下是一个图形,它可以帮助你在脑海中形成这个概念:

图片

线程的简单图形

在谈论线程时,最重要的一点是:机器如何确定在某个时刻处理哪个线程?

众所周知,我们的机器资源(CPU、RAM)是有限的,因此正确确定我们将资源分配在哪里非常重要,或者说,哪些操作应该优先于其他操作。这一切都必须实现,而且同时,需要确保没有任何操作耗费太多时间,因为没有人喜欢笔记本电脑速度过慢。

用于解决资源分配问题的机制就叫作调度,它由我们的操作系统中称为 OS 调度程序的实体来管理。这背后的逻辑可能非常复杂,但总而言之,我们可以将执行此操作的两大方法组合在一起:

图片

多核机器如何处理线程
  • 使用优化逻辑,以减少死锁时间:这是对我们来说最切实的方法。如果我们仔细研究一下线程是如何工作的,我们就会看到 OS 调度程序可以识别出 CPU 何时在等待其他资源来执行某个作业,以便分配这个资源来同时执行其他操作。这通常发生在非常昂贵的 I/O 操作上,例如硬盘读取。

事件循环

现在我们已经对线程的工作原理有了新的了解,我们终于可以解决Node.js 事件循环逻辑了。通过阅读本文,你会了解前面的解释它背后的原因,而且每个部分都会自行找到正确的位置。

每当我们运行 Node 程序时,都会自动创建一个线程。这个线程就是我们整个代码库被执行的唯一地方。其中,还生成了一个称为事件循环的东西。这个循环的作用是安排我们唯一的线程在某个给定的时间点应该执行哪些操作。

请注意:在我们运行了程序后那一刹那,事件循环不会立刻生成。实际上,只有在整个程序执行完毕后事件循环才会运行。

详情

现在让我们尝试模拟事件循环的工作原理以及它如何使我们的程序开始工作。为此,我将假装自己正在使用一个名为 myProgram 的文件为 Node 提供信息,然后我们再详细了解事件循环将执行的所有操作。

图片

特别的,我将首先编写一个简短的图形解释,来说明在某个事件循环 tick 过程中发生了什么,然后我将以更深入的方式探讨这些阶段。

图片

事件循环的图形说明

第 1 步:performChecks

我不应该告诉你事件循环实际上是一个循环。这意味着它有一个特定的条件,这个条件将决定循环是否需要再次迭代。事件循环的每次迭代都称为tick

事件循环执行 tick 的条件是什么?

每当我们执行程序时,我们都会有一系列需要执行的操作。这些操作可分为三个大类:

  • 挂起的定时器操作 (setTimeout(), setInterval(),setImmediate())

  • 挂起的操作系统 (OS) 任务

  • 挂起的长时间运行操作的执行

我们稍后会详细介绍这些内容;现在,让我们记住,只要其中一个操作处于挂起状态,事件循环就会执行一个新的 tick。

第 2 步:执行 tick

对于每个循环迭代,我们可以将其分为以下阶段:

  • 阶段 1:Node 查看挂起的计时器的内部集合,并检查传递给setTimeout()setInterval()的回调函数是否准备好在计时器过期的情况下被调用。

  • 阶段 2:Node 查看挂起的 OS 任务的内部集合,并检查哪些回调函数已准备好被调用。从机器的硬盘驱动器中检索文件即是一个例子。

  • 阶段 3:Node 暂停执行,等待新事件的出现。新事件包括:新的计时器完成、新的 OS 任务完成和新的挂起操作完成。

  • 阶段 4:Node 检查是否准备好调用与挂起定时器(挂起定时器与setImmediate()函数相关)相关的任何函数。

  • 阶段 5:管理关闭事件,用于清理应用程序的状态。

关于事件循环的常见问题和错误认识

Node.js 是完全单线程的吗?

这是对这项技术的一种非常普遍的误解。虽然 Node 是在单个线程上运行,但是 Node.js 标准库中包含的有些函数并不是如此(例如 fs 模块函数);它们的逻辑运行在 Node.js 单线程之外,这样做是为了保持程序的速度和性能。

这些其他线程在哪里外包?

使用 Node.js 时,会使用一个名为libuv的特殊库模块来执行异步操作。此库还与 Node 的后向逻辑一起被用来管理称为libuv 线程池的特殊线程池。

此线程池由四个线程组成,它们负责委派对事件循环来说太繁重的操作。上述事件循环逻辑中的长时间运行的任务代表此处所描述的对于事件循环而言过于昂贵的操作。

那么事件循环是一种类似堆栈的结构吗?

从这个意义上说,虽然在上述过程中涉及到了一些类似堆栈的结构,但更准确的答案是事件循环由一系列阶段组成,每个阶段都有自己的特定任务,而且所有阶段都以循环重复的方式被处理。有关事件循环确切结构的更多信息,请看这个对话

结论

了解事件循环是使用 Node.js 的重要部分,无论你是想获得有关此技术的更多见解、了解如何提高其性能,还是希望找到学习一个新工具的新的且有趣的理由。

本指南应当能帮助你探索这个主题。请在下面发表评论,你的意见和反馈将有助于每个人更好地学习。

英文原文: https://blog.logrocket.com/a-complete-guide-to-the-node-js-event-loop/

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论