译者按: Node.js 文档阅读系列之一。
- 原文: Overview of Blocking vs Non-Blocking
- 译者: Fundebug
为了保证可读性,本文采用意译而非直译。
这篇博客将介绍 Node.js 的阻塞 (Blocking) 与非阻塞 (Non-Blocking)。我会提到 Event Loop 与 libuv,但是不了解它们也不会影响阅读。读者只需要有一定的 JavaScript 基础,理解 Node.js 的回调函数(callback pattern) 就可以了。
博客中提到了很多次 I/O,它主要指的是 使用 libuv 与系统的磁盘与网络进行交互。
阻塞(Blocking)
阻塞 指的是一部分 Node.js 代码需要等到一些非 Node.js 代码执行完成之后才能继续执行。这是因为当阻塞发生时,Event Loop 无法继续执行。
对于 Node.js 来说,由于 CPU 密集的操作导致代码性能很差时,不能称为阻塞。当需要等待非 Node.js 代码执行时,才能称为阻塞。Node.js 中依赖于 libuv 的同步方法 (以 Sync 结尾) 导致阻塞,是最常见的情况。当然,一些不依赖于 libuv 的原生 Node.js 方法有些也能导致阻塞。
Node.js 中所有与 I / O 相关的方法都提供了异步版本,它们是 非阻塞 的,可以指定回调函数,例如 fs.readFile。其中一些方法也有对应的 阻塞 版本,它们的函数名以 Sync 结尾,例如 fs.readFileSync。
代码示例
阻塞的方法是同步执行的,而非阻塞的方法是异步执行。
以读文件为例,下面是同步执行的代码:
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // 文件读取完成之前,代码会阻塞,不会执行后面的代码
console.log("Hello, Fundebug!"); // 文件读取完成之后才会打印
对应的异步代码如下:
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {if (err) throw err;
}); // 代码不会因为读文件阻塞,会继续执行后面的代码
console.log("Hello, Fundebug!"); // 文件读完之前就会打印
第一个示例代码看起来要简单很多,但是它的缺点是会阻塞代码执行,后面的代码需要等到整个文件读取完成之后才能继续执行。
在同步代码中,如果读取文件出错了,则错误需要使用 try…catch 处理,否则进程会崩溃。对于异步代码,是否处理回调函数的错误则取决于开发者。
我们可以将示例代码稍微修改一下,下面是同步代码:
const fs = require('fs');
const data = fs.readFileSync('/file.md');
console.log(data);
moreWork(); // console.log 之后再执行
异步代码如下:
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {if (err) throw err;
console.log(data);
});
moreWork(); // 先于 console.log 执行
在第一个示例中,console.log
将会先于 moreWork()
执行。在第二个示例中,由于 fs.readFile()
是非阻塞的,代码可以继续执行,因此 moreWork()
会先于 console.log
执行。
moreWork()
不用等待读取整个文件,可以继续执行,这是 Node.js 可以增加吞吐量的关键。
并发与吞吐量
Node.js 中 JS 代码执行是单线程的,因此并发指的是 Event Loop 可以在执行其他代码之后再去执行回调函数。如果希望代码可以并发执行,则所有非 JavaScript 代码比如 I / O 执行时,必须保证 Event Loop 继续运行。
举个例子,假设 Web 服务器的每个请求需要 50ms 完成,其中 45ms 是数据库的 I / O 操作。如果使用非阻塞的异步方式执行数据库 I / O 的话,则可以节省 45ms 来处理其他请求,这可以极大地提高系统的吞吐量。
Event Loop 这种方式与其他许多语言都不一样,通常它们会创建新的线程来处理并发。
混用阻塞与非阻塞代码会出问题
当我们处理 I / O 时,应该避免以下代码:
const fs = require('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('fs');
fs.readFile('/file.md', (readFileErr, data) => {if (readFileErr) throw readFileErr;
console.log(data);
fs.unlink('/file.md', (unlinkErr) => {if (unlinkErr) throw unlinkErr;
});
});
上面的示例中,我们把非阻塞的 fs.unlink()
放在 fs.readFile()
的回调函数中。
参考
- libuv
- About Node.js
关于 Fundebug
Fundebug 专注于 JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js 和 Java 线上应用实时 BUG 监控。自从 2016 年双十一正式上线,Fundebug 累计处理了 10 亿 + 错误事件,付费客户有 Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!
版权声明
转载时请注明作者 Fundebug 以及本文地址:
https://blog.fundebug.com/2019/06/12/overview-of-nodejs-blocking-vs-non-blocking/