阻塞与非阻塞概述
本概述涵盖了 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()
的回调函数中,这保证了操作的正确顺序。