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