相关文章推荐
体贴的黄瓜  ·  Elasticsearch:Cluster备 ...·  2 年前    · 
爽快的包子  ·  WPF中C#代码触发鼠标点击事件_51CTO ...·  2 年前    · 
风流的韭菜  ·  使用Diffie-Hellman算法(非对称 ...·  2 年前    · 
千杯不醉的啄木鸟  ·  hive 去掉字符串中的数字-掘金·  2 年前    · 
睿智的眼镜  ·  通过命令行创建 Python 函数 - ...·  2 年前    · 
Code  ›  NodeJS 的事件循环详解 | 众里千寻
libuv 文件句柄 node
https://everfind.github.io/posts/2021/08/15/nodejs-event-loop.html
虚心的海龟
5 月前
众里千寻 众里千寻
学习笔记
解决方案
开发利器
源码解读
最新消息
GitHub open in new window
学习笔记
解决方案
开发利器
源码解读
最新消息
GitHub open in new window

    NodeJS 的事件循环详解

    • libuv 的事件循环
    • NodeJS 的事件循环
    • setImmediate、setTimeout 和 process.nextTick
    • Promise
    • 总结

事件循环是 NodeJS 处理非阻塞 I/O 操作的和核心机制。NodeJS 的事件循环脱胎于 libuv open in new window 的事件循环,因此,要搞清楚 NodeJS 的事件循环,还需要先了解 libuv 的事件循环是如何工作的。

# libuv 的事件循环

我们先来了解两个基本概念:句柄(handle)和请求(request).

  • 句柄是指在整个事件循环活跃时间内能够执行某些操作的长期对象。比如一个 TCP 服务句柄,每当有新的联接建立时,这个句柄的 connected 回调就会被调用。
  • 请求是通常指短期操作。比如向某个句柄中写入数据的操作。

了解了这两个概念以后,我们来看看 libuv 的事件循环是如何工作的。

下面这张图可以清楚的展示事件循环的执行过程:

libuv 的事件循环

结合这张图我们简单描述一下一次循环过程中各个步骤做了什么。

  1. 首先更新循环内的当前时间(now),避免在循环过程中多次发生与时间相关的系统调用。
  2. 检查当前事件循环是否还是活跃(active)的。检查的表示是当前事件循环是否还有活跃的句柄、活跃的请求操作,或者还有“关闭”回调的话,就视为是活跃的。如果判断当前循环不是活跃的,则直接退出。
  3. 执行所有的到期回调。即所有的到期时间在循环当前时间之前的回调都会被执行。
  4. 执行所有的挂起回调(pending callbacks)。所谓挂起回调,就是在上一个循环周期中设置的到下一循环周期在执行的回调。
  5. 执行空闲句柄回调(idle handle callbacks)。虽然名字中包含空闲二字,实际上每个循环周期都会执行。
  6. 执行准备句柄回调(prepare handle callbacks)。
  7. 在这一步会暂停循环,轮询等待 I/O 事件一段时间。这个时间长度是根据一个算法算出,这里不做详细说明。在轮询期间,所有 I/O 相关的回调会被执行(前提是系统通知到 libuv)。
  8. 执行检查句柄回调(check handle callbacks)。检查句柄回调往往与准备句柄回调相对应。这两个回调可以方便我们在 I/O 之前做一些准备工作,然后在 I/O 之后做相应的检查。
  9. 执行关闭回调(close callbacks)。比如通过 uv_close() 设置的回调。

整个事件循环就是 1 - 9 的循环执行。

值得说明的是,libuv 会在轮询阶段中断事件循环,等待系统通知。比如某个文件 I/O 已经完成,或者接收到一个网络连接等。在接收到系统通知后,事件循环会调用相关的回调执行操作。

不同的平台(windows\linux 等),异步 I/O 的机制不同,libuv 底层会根据不同平台,采用不同的 I/O 轮询机制,比如 epoll(linux)、kqueue(OSX)、IOCP(windows)等,上层不需要关注异步 I/O 的实现机制。

# NodeJS 的事件循环

现在我们来看 NodeJS 的事件循环。同样,我们放一张 NodeJS 事件循环的过程图。

NodeJS 的事件循环

在 NodeJS 中,事件循环的每一步成为一个阶段,每个阶段都有一个 FIFO 队列来执行回调。通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列清空或达到最大回调数限制。当队列清空或者达到最大限制,事件循环进入下一阶段。

对比两个事件循环的图,我们可以看到,具体过程基本相同。因此,NodeJS 的事件循环过程我们简述如下:

  1. 定时器阶段,执行已经被 setTimeout() 和 setInterval() 调度的回调函数。
  2. 挂起的回调,执行(在上一个循环中被设置)延迟到下一个循环迭代的 I/O 回调。
  3. idle, prepare 阶段,仅 NodeJS 系统内部使用。
  4. 轮询阶段,检索新的 I/O 事件,执行与 I/O 相关的回调。与 libuv 一样,NodeJS 还在这个阶段暂停循环一段时间。
  5. 检测阶段,执行被 setImmediate() 调度的回调函数。
  6. 关闭的回调函数,执行一些关闭的回调函数,如: socket.on('close', ...) 。

我们对轮询阶段做个详细说明。

轮询阶段有两个重要的功能:

  • 计算应该阻塞和轮询 I/O 的时间。
  • 处理轮询队列里的事件。

一旦事件循环进入轮询阶段并且没有到期的定时器回调时,事件循环将做如下判断:

  • 如果轮询队列不是空的,那么事件循环将循环访问回调队列并同步执行它们,直到清空队列,或者达到了最大限制。
  • 如果轮询队列是空的,则再做如下判断:
    • 如果有代码是被 setImmediate() 调度的,那么事件循环将结束轮询阶段,并到检查阶段以执行那些被调度的代码。
    • 如果没有代码被 setImmediate() 调度,那么事件循环将等待回调被添加到队列中,然后立即执行。

在轮询阶段的执行过程中,一旦轮询队列为空,事件循环将检查是否有到期的定制器。如果一个或多个定时器已准备就绪,则事件循环将绕回定时器阶段以执行这些定时器的回调。

这里要特别对 setImmediate() 进行一些说明。

在 libuv 的事件循环中,允许开发人员在轮询阶段之前做些准备操作,然后在轮询阶段之后立即对这些操作进行检查。NodeJS 中 setImmediate() 实际上是一个在事件循环的单独阶段运行的特殊定时器。它使用一个 libuv API 来安排回调在轮询阶段完成后执行。

# setImmediate 、 setTimeout 和 process.nextTick

  • setImmediate() 被设计为一旦在当前轮询阶段完成,就执行代码。
  • setTimeout() 是在最小阈值(ms 单位)过后执行代码。
  • process.nextTick() 严格意义上讲并不属于事件循环的一部分。它不管事件循环的当前阶段如何,它都将在当前操作完成后处理 nextTickQueue 中排队的代码。

setImmediate() 和 setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

我们看下面这段代码:

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

这两个函数调用都在主模块中被调用,则他们的回调执行顺序是不定的,受进程的性能影响很大(进程会受到系统中运行其他应用程序影响)。

但是一旦将这两个函数放到 I/O 轮询调用内,那么 setImmediate() 一定会在 setTimeout() 之前被执行,不管有多个定制器已经到期。比如下面这段代码,总是会先输出 "immediate"。

const fs = require('fs');
fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
1
2
3
4
5
6
7
8
9
10

process.nextTick() 和 setImmediate() 严格意义上来说,应该将名称互换。因为 process.nextTick() 比 setImmediate() 触发得更快。

任何时候在给定的阶段中调用 process.nextTick() ,所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。之所以这么设计,是考虑到这些使用场景:

  • 允许开发者处理错误,清理任何不需要的资源,或者在事件循环继续之前重试请求。
  • 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。

比如下面这段代码:

const server = net.createServer(() => {}).listen(8080);
 
推荐文章
体贴的黄瓜  ·  Elasticsearch:Cluster备份 Snapshot及Restore API - 哈喽哈喽111111 - 博客园
2 年前
爽快的包子  ·  WPF中C#代码触发鼠标点击事件_51CTO博客_c# 获取鼠标当前位置
2 年前
风流的韭菜  ·  使用Diffie-Hellman算法(非对称加密算法)得到对称加密需要的秘钥_diffie-hellman算法的应用案例分析_Scoful的博客-CSDN博客
2 年前
千杯不醉的啄木鸟  ·  hive 去掉字符串中的数字-掘金
2 年前
睿智的眼镜  ·  通过命令行创建 Python 函数 - Azure Functions | Microsoft Learn
2 年前
今天看啥   ·   Py中国   ·   codingpro   ·   小百科   ·   link之家   ·   卧龙AI搜索
删除内容请联系邮箱 2879853325@qq.com
Code - 代码工具平台
© 2024 ~ 沪ICP备11025650号