阻塞与非阻塞概述

本概述涵盖了 Node.js 中 **阻塞** 和 **非阻塞** 调用的区别。本概述将涉及事件循环和 libuv,但不需要事先了解这些主题。假设读者对 JavaScript 语言和 Node.js 的 回调模式 有基本了解。

"I/O" 主要指与系统磁盘和网络的交互,由 libuv 支持。

阻塞

**阻塞** 指的是 Node.js 进程中额外的 JavaScript 执行必须等待非 JavaScript 操作完成。这是因为事件循环在 **阻塞** 操作发生时无法继续运行 JavaScript。

在 Node.js 中,由于 CPU 密集型操作而不是等待非 JavaScript 操作(例如 I/O)而导致性能不佳的 JavaScript 通常不被视为 **阻塞**。Node.js 标准库中使用 libuv 的同步方法是最常用的 **阻塞** 操作。原生模块也可能具有 **阻塞** 方法。

Node.js 标准库中的所有 I/O 方法都提供异步版本,它们是 **非阻塞** 的,并接受回调函数。某些方法也具有 **阻塞** 对应项,其名称以 Sync 结尾。

代码比较

**阻塞** 方法 **同步** 执行,而 **非阻塞** 方法 **异步** 执行。

以文件系统模块为例,这是一个 **同步** 文件读取

const fs = require('node:fs');

const data = fs.readFileSync('/file.md'); // blocks here until file is read

这是一个等效的 **异步** 示例

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
});

第一个示例看起来比第二个示例更简单,但它有一个缺点,即第二行会 **阻塞** 任何其他 JavaScript 代码的执行,直到整个文件被读取。请注意,在同步版本中,如果抛出错误,则需要捕获它,否则进程将崩溃。在异步版本中,由作者决定是否应该抛出错误,如所示。

让我们稍微扩展一下我们的示例

const fs = require('node:fs');

const data = fs.readFileSync('/file.md'); // blocks here until file is read
console.log(data);
moreWork(); // will run after console.log

这是一个类似但不等效的异步示例

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
moreWork(); // will run before console.log

在上面的第一个示例中,console.log 将在 moreWork() 之前被调用。在第二个示例中,fs.readFile() 是 **非阻塞** 的,因此 JavaScript 执行可以继续,并且 moreWork() 将首先被调用。在不等待文件读取完成的情况下运行 moreWork() 的能力是允许更高吞吐量的关键设计选择。

并发和吞吐量

Node.js 中的 JavaScript 执行是单线程的,因此并发是指事件循环在完成其他工作后执行 JavaScript 回调函数的能力。任何预期以并发方式运行的代码都必须允许事件循环在非 JavaScript 操作(如 I/O)发生时继续运行。

例如,让我们考虑一个每个请求到 Web 服务器都需要 50 毫秒才能完成,其中 45 毫秒是数据库 I/O,可以异步完成的情况。选择**非阻塞**异步操作可以释放每个请求的 45 毫秒来处理其他请求。仅仅通过选择使用**非阻塞**方法而不是**阻塞**方法,就可以显著提高容量。

事件循环与许多其他语言中的模型不同,在其他语言中,可能会创建额外的线程来处理并发工作。

混合阻塞和非阻塞代码的风险

在处理 I/O 时,有一些模式应该避免。让我们看一个例子

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
  console.log(data);
});
fs.unlinkSync('/file.md');

在上面的例子中,fs.unlinkSync() 可能会在 fs.readFile() 之前运行,这会导致在实际读取之前删除 file.md。一个更好的方法是完全**非阻塞**并保证按正确顺序执行,如下所示

const fs = require('node:fs');

fs.readFile('/file.md', (readFileErr, data) => {
  if (readFileErr) throw readFileErr;
  console.log(data);
  fs.unlink('/file.md', unlinkErr => {
    if (unlinkErr) throw unlinkErr;
  });
});

上面的代码将对 fs.unlink() 的**非阻塞**调用放在 fs.readFile() 的回调函数中,这保证了操作的正确顺序。

其他资源