阻塞与非阻塞概述

¥Overview of Blocking vs Non-Blocking

本概述涵盖了 Node.js 中阻塞和非阻塞调用之间的区别。本概述将参考事件循环和 libuv,但不需要事先了解这些主题。假设读者对 JavaScript 语言和 Node.js 回调模式 有基本的了解。

¥This overview covers the difference between blocking and non-blocking calls in Node.js. This overview will refer to the event loop and libuv but no prior knowledge of those topics is required. Readers are assumed to have a basic understanding of the JavaScript language and Node.js callback pattern.

"I/O" 主要指与 libuv 支持的系统磁盘和网络的交互。

¥"I/O" refers primarily to interaction with the system's disk and network supported by libuv.

阻塞

¥Blocking

阻塞是指 Node.js 进程中其他 JavaScript 的执行必须等到非 JavaScript 操作完成。发生这种情况的原因是,在发生阻塞操作时,事件循环无法继续运行 JavaScript。

¥Blocking is when the execution of additional JavaScript in the Node.js process must wait until a non-JavaScript operation completes. This happens because the event loop is unable to continue running JavaScript while a blocking operation is occurring.

在 Node.js 中,由于占用大量 CPU 而不是等待非 JavaScript 操作(例如 I/O)而表现出较差性能的 JavaScript 通常不被称为阻塞。Node.js 标准库中使用 libuv 的同步方法是最常用的阻塞操作。原生模块也可能有阻塞方法。

¥In Node.js, JavaScript that exhibits poor performance due to being CPU intensive rather than waiting on a non-JavaScript operation, such as I/O, isn't typically referred to as blocking. Synchronous methods in the Node.js standard library that use libuv are the most commonly used blocking operations. Native modules may also have blocking methods.

Node.js 标准库中的所有 I/O 方法都提供异步版本,它们是非阻塞的,并接受回调函数。某些方法也有阻塞对应项,其名称以 Sync 结尾。

¥All of the I/O methods in the Node.js standard library provide asynchronous versions, which are non-blocking, and accept callback functions. Some methods also have blocking counterparts, which have names that end with Sync.

比较代码

¥Comparing Code

阻塞方法同步执行,非阻塞方法异步执行。

¥Blocking methods execute synchronously and non-blocking methods execute asynchronously.

以文件系统模块为例,这是一个同步文件读取:

¥Using the File System module as an example, this is a synchronous file read:

const fs = require('node:fs');

const data = fs.readFileSync('/file.md'); // blocks here until file is read

这是一个等效的异步示例:

¥And here is an equivalent asynchronous example:

const fs = require('node:fs');

fs.readFile('/file.md', (err, data) => {
  if (err) throw err;
});

第一个例子看起来比第二个简单,但缺点是第二行会阻止执行任何其他 JavaScript,直到读取整个文件。请注意,在同步版本中,如果抛出错误,则需要捕获该错误,否则进程将崩溃。在异步版本中,是否抛出错误由作者决定,如下所示。

¥The first example appears simpler than the second but has the disadvantage of the second line blocking the execution of any additional JavaScript until the entire file is read. Note that in the synchronous version if an error is thrown it will need to be caught or the process will crash. In the asynchronous version, it is up to the author to decide whether an error should throw as shown.

让我们稍微扩展一下我们的例子:

¥Let's expand our example a little bit:

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

这是一个类似但不等效的异步示例:

¥And here is a similar, but not equivalent asynchronous example:

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() 的能力是实现更高吞吐量的关键设计选择。

¥In the first example above, console.log will be called before moreWork(). In the second example fs.readFile() is non-blocking so JavaScript execution can continue and moreWork() will be called first. The ability to run moreWork() without waiting for the file read to complete is a key design choice that allows for higher throughput.

并发性和吞吐量

¥Concurrency and Throughput

Node.js 中的 JavaScript 执行是单线程的,因此并发是指事件循环在完成其他工作后执行 JavaScript 回调函数的能力。任何预期以并发方式运行的代码都必须允许事件循环在发生非 JavaScript 操作(如 I/O)时继续运行。

¥JavaScript execution in Node.js is single threaded, so concurrency refers to the event loop's capacity to execute JavaScript callback functions after completing other work. Any code that is expected to run in a concurrent manner must allow the event loop to continue running as non-JavaScript operations, like I/O, are occurring.

例如,让我们考虑这样一种情况,其中对 Web 服务器的每个请求需要 50 毫秒才能完成,其中 45 毫秒是可以异步完成的数据库 I/O。选择非阻塞异步操作可释放每个请求的 45ms 来处理其他请求。仅通过选择使用非阻塞方法而不是阻塞方法,容量就会有显著差异。

¥As an example, let's consider a case where each request to a web server takes 50ms to complete and 45ms of that 50ms is database I/O that can be done asynchronously. Choosing non-blocking asynchronous operations frees up that 45ms per request to handle other requests. This is a significant difference in capacity just by choosing to use non-blocking methods instead of blocking methods.

事件循环与许多其他语言中的模型不同,在这些语言中,可能会创建额外的线程来处理并发工作。

¥The event loop is different than models in many other languages where additional threads may be created to handle concurrent work.

混合阻塞和非阻塞代码的危险

¥Dangers of Mixing Blocking and Non-Blocking Code

在处理 I/O 时,应该避免某些模式。让我们看一个例子:

¥There are some patterns that should be avoided when dealing with I/O. Let's look at an example:

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。更好的编写方法是完全非阻塞的,并保证按正确的顺序执行:

¥In the above example, fs.unlinkSync() is likely to be run before fs.readFile(), which would delete file.md before it is actually read. A better way to write this, which is completely non-blocking and guaranteed to execute in the correct order is:

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() 的非阻塞调用,从而保证了正确的操作顺序。

¥The above places a non-blocking call to fs.unlink() within the callback of fs.readFile() which guarantees the correct order of operations.

其他资源

¥Additional Resources