阻塞与非阻塞概述
本概述介绍了 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 = ('node:fs');
const = .('/file.md'); // blocks here until file is read
这是一个等效的异步示例:
const = ('node:fs');
.('/file.md', (, ) => {
if () {
throw ;
}
});
第一个示例看起来比第二个简单,但缺点是第二行会阻塞任何其他 JavaScript 的执行,直到整个文件被读取。请注意,在同步版本中,如果抛出错误,需要进行捕获,否则进程将崩溃。在异步版本中,由作者决定是否应该抛出错误,如示例所示。
让我们稍微扩展一下我们的示例:
const = ('node:fs');
const = .('/file.md'); // blocks here until file is read
.();
moreWork(); // will run after console.log
这是一个相似但不等效的异步示例:
const = ('node:fs');
.('/file.md', (, ) => {
if () {
throw ;
}
.();
});
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 = ('node:fs');
.('/file.md', (, ) => {
if () {
throw ;
}
.();
});
.('/file.md');
在上面的例子中,fs.unlinkSync() 很可能在 fs.readFile() 之前运行,这将在文件实际被读取之前删除 file.md。一个更好的写法是完全非阻塞且保证按正确顺序执行的方式:
const = ('node:fs');
.('/file.md', (, ) => {
if () {
throw ;
}
.();
.('/file.md', => {
if () {
throw ;
}
});
});
以上代码将一个非阻塞的 fs.unlink() 调用放在 fs.readFile() 的回调函数中,这保证了操作的正确顺序。