阻塞与非阻塞概述
此概述涵盖了 Node.js 中阻塞和非阻塞调用之间的区别。 本概述将涉及事件循环和 libuv,但不需要事先了解这些主题。 假定读者对 JavaScript 语言和 Node.js 回调模式 有基本的了解。
"I/O"主要是指与系统磁盘的交互, libuv 支持的网络。
阻塞
阻塞 是指在 Node.js 进程中执行其他 JavaScript 必须等到非 JavaScript 操作完成。 发生这种情况是因为事件循环无法在 阻塞操作正在进行。
在 Node.js 中,由于 CPU 密集而不是等待非 JavaScript 操作(例如 I/O)而表现出较差性能的 JavaScript 通常不称为 阻塞。 Node.js 标准库中使用 libuv 的同步方法是最常用的阻塞操作。 原生模块也可能有 blocking 方法。
Node.js 标准库中的所有 I/O 方法都提供了异步版本,它是非阻塞的,并接受回调函数。 一些方法也有 blocking 对应物,它们的名称以 Sync
结尾。
比较代码
阻塞方法执行同步,非阻塞方法执行异步。
以文件系统模块为例,这是一个同步文件读取:
这是一个等效的异步示例:
第一个示例看起来比第二个简单,但第二行的缺点是阻塞任何其他 JavaScript 的执行,直到整个文件被读取。 请注意,在同步版本中,如果抛出错误,则需要将其捕获,否则进程将崩溃。 在异步版本中,由作者决定是否应如图所示抛出错误。
让我们稍微扩展一下示例:
这是一个类似但不等效的异步示例:
在上面的第一个示例中,console.log
将在 moreWork()
之前被调用。 在第二个示例中,fs.readFile()
是非阻塞,因此 JavaScript 可以继续执行,moreWork()
将首先被调用。 无需等待文件读取完成即可运行 moreWork()
的能力是允许更高吞吐量的关键设计选择。
并发和吞吐量
Node.js 中的 JavaScript 执行是单线程的,因此并发是指事件循环在完成其他工作后执行 JavaScript 回调函数的能力。 任何预期以并发方式运行的代码都必须允许事件循环在非 JavaScript 操作(如 I/O)发生时继续运行。
例如,让我们考虑这样一种情况:每个对 Web 服务器的请求都需要 50 毫秒才能完成,而这 50 毫秒中有 45 毫秒是可以异步完成的数据库 I/O。 选择非阻塞异步操作可以释放每个请求 45 毫秒的时间来处理其他请求。 仅通过选择使用 non-blocking 方法而不是 阻塞方法。
事件循环不同于许多其他语言中的模型,在这些模型中可以创建额外的线程来处理并发工作。
混合阻塞和非阻塞代码的危险
在处理 I/O 时,应该避免一些模式。 让我们看一个例子:
在上面的示例中,fs.unlinkSync()
很可能在 fs.readFile()
之前运行,这将在实际读取之前删除 file.md
。 一个更好的写法,它是完全非阻塞并且保证以正确的顺序执行的:
上面在 fs.readFile()
的回调中放置了对 fs.unlink()
的非阻塞调用,以保证正确的操作顺序。