本概述涵盖了 Node.js 中阻塞与非阻塞调用的区别。本概述将提及事件循环和 libuv,但不需要事先了解这些主题。假定读者对 JavaScript 语言和 Node.js 回调模式有基础的了解。
“I/O” 主要指与系统磁盘和网络的交互,这些交互由 libuv 提供支持。
阻塞是指 Node.js 进程中后续 JavaScript 的执行必须等待非 JavaScript 操作完成。这是因为当阻塞操作发生时,事件循环无法继续运行 JavaScript。
在 Node.js 中,如果 JavaScript 因 CPU 密集型任务(而不是等待非 JavaScript 操作,如 I/O)导致性能不佳,通常不会被称为阻塞。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 回调函数的能力。任何预期以并发方式运行的代码都必须允许事件循环在进行 I/O 等非 JavaScript 操作时继续运行。
例如,假设 web 服务器的每个请求需要 50ms 完成,其中 45ms 是可以异步完成的数据库 I/O。选择非阻塞异步操作可以将每个请求中原本被占用的 45ms 释放出来,以处理其他请求。仅通过选择使用非阻塞方法代替阻塞方法,容量就会有显著差异。
事件循环不同于许多其他语言的模型,在那些语言中,可能会创建额外的线程来处理并发工作。
在处理 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.readFile() 的回调中放置了一个对 fs.unlink() 的非阻塞调用,这保证了操作的正确顺序。