阻塞与非阻塞概述
本概述介绍了 Node.js 中阻塞调用和非阻塞调用的区别。本概述将涉及事件循环和 libuv,但不需要预先了解这些主题。读者应该对 JavaScript 语言和 Node.js 的 回调模式 有基本的了解。
INFO
“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') // 在此处阻塞,直到文件读取完毕
这是一个等效的异步示例:
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') // 在此处阻塞,直到文件读取完毕
console.log(data)
moreWork() // 将在 console.log 之后运行
这是一个类似的,但不等效的异步示例:
const fs = require('node:fs')
fs.readFile('/file.md', (err, data) => {
if (err) throw err
console.log(data)
})
moreWork() // 将在 console.log 之前运行
在上面的第一个示例中,console.log
将在 moreWork()
之前被调用。在第二个示例中,fs.readFile()
是非阻塞的,因此 JavaScript 执行可以继续,moreWork()
将首先被调用。无需等待文件读取完成即可运行 moreWork()
的能力是一个关键的设计选择,它允许实现更高的吞吐量。
并发和吞吐量
Node.js 中的 JavaScript 执行是单线程的,因此并发指的是事件循环在完成其他工作后执行 JavaScript 回调函数的能力。任何期望以并发方式运行的代码都必须允许事件循环在进行非 JavaScript 操作(如 I/O)时继续运行。
例如,让我们考虑一下这种情况:每个对 Web 服务器的请求需要 50 毫秒才能完成,而这 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.readFile()
的回调函数中放置了一个非阻塞的 fs.unlink()
调用,这保证了操作的正确顺序。