跳到内容

阻塞与非阻塞概述

🌐 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 提供支持。

阻塞

🌐 Blocking

阻塞是指在 Node.js 进程中执行额外的 JavaScript 必须等待非 JavaScript 操作完成的情况。之所以会发生这种情况,是因为事件循环在阻塞操作发生时无法继续运行 JavaScript。

在 Node.js 中,JavaScript 性能差通常是由于 CPU 密集型操作而不是等待非 JavaScript 操作(例如 I/O)所致,这通常不被称为阻塞。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

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

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

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

const  = ('node:fs');

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

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

🌐 And here is an equivalent asynchronous example:

const  = ('node:fs');

.('/file.md', (, ) => {
  if () {
    throw ;
  }
});

第一个示例看起来比第二个简单,但有一个缺点,即第二行会阻塞任何额外 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  = ('node:fs');

const  = .('/file.md'); // blocks here until file is read
.();
moreWork(); // will run after console.log

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

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

const  = ('node:fs');

.('/file.md', (, ) => {
  if () {
    throw ;
  }

  .();
});
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 毫秒,其中 50 毫秒中的 45 毫秒是可以异步完成的数据库 I/O。选择非阻塞异步操作可以释放每个请求的 45 毫秒,用于处理其他请求。仅仅通过选择使用非阻塞方法而不是阻塞方法,这在处理能力上就有显著差异。

🌐 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  = ('node:fs');

.('/file.md', (, ) => {
  if () {
    throw ;
  }

  .();
});
.('/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  = ('node:fs');

.('/file.md', (, ) => {
  if () {
    throw ;
  }

  .();

  .('/file.md',  => {
    if () {
      throw ;
    }
  });
});

上述代码在 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