Node.js 并不是单线程的,它使用了事件循环(Event Loop)机制来处理并发任务
是的 和 不是。是的,它的主线程确实是单线程的,因为 JavaScript 本身就是在单线程上运行的。你编写的所有代码都在这个主线程上运行,这也驱动了著名的事件循环。然而,误解在于认为 Node.js 所做的一切都只是在这个单一的主线程上运行。实际上,Node.js 的很大一部分是多线程的,但这发生在 JavaScript 直接执行的范围之外。
V8 和 libuvNode.js 是基于 Google 的 V8 引擎 构建的,该引擎将 JavaScript 编译为原生机器码指令。然而,在处理文件系统 (fs
)、加密 (crypto
) 或 HTTP 等功能时,Node.js 则使用一个名为 libuv 的库。Libuv 提供了对操作系统层级功能的访问,包括线程。它也是管理事件循环的库——让异步操作变得简单神奇的地方。
当 Node.js 需要执行一些任务时(如访问文件系统或执行加密操作),它会利用 libuv,libuv 管理一个 线程池(thread pool)。这个线程池负责处理这些繁重的任务,让 JavaScript 能够继续顺畅运行。
libuv的组件
每个线程在这个池中都可以独立并发地执行任务。虽然这发生在 JavaScript 运行时之外,但它仍然是你的(或)Node.js 进程的一部分。这意味着 Node.js 可以同时处理多个耗时任务而不会冻结你的主线程。
您可以通过设置环境变量 UV_THREADPOOL_SIZE
来控制该线程池的大小:在您的 JavaScript 代码里。
// 将默认线程池大小从4提升到6
process.env.UV_THREADPOOL_SIZE = 6;
让我们看看哈希的魔法是如何工作的:一个实际的例子
考虑一个例子,如下:使用Node.js的crypto
模块执行高强度哈希操作的例子,让我们来展示这一点。
const { scrypt } = require('crypto');
const start = Date.now();
const computeHash = () => {
scrypt('password', 'salt', 64, (err, derivedKey) => {
if (err) throw err;
console.log(`哈希计算完成,耗时 ${Date.now() - start}ms`);
});
};
computeHash(); // 哈希计算完成,耗时大约 500 毫秒
在这个例子中,如果我们多次调用 computeHash()
函数,我们可能会期望每次操作都会阻塞线程并顺序运行。但让我们看看连续调用四次会发生什么:
computeHash(); // 哈希计算耗时 501毫秒
computeHash(); // 哈希计算耗时 503毫秒
computeHash(); // 哈希计算耗时 505毫秒
computeHash(); // 哈希计算耗时 507毫秒
令人惊讶的是,这些操作可以同时进行,并且在差不多相同的时间内完成!这背后的原因是Node.js将哈希计算的任务交给libuv线程池处理。
但如果我们把线程池大小降到一呢:
process.env.UV_THREADPOOL_SIZE = 1; // 设置UV线程池大小为1
computeHash(); // 哈希计算耗时 501毫秒
computeHash(); // 哈希计算耗时 1004毫秒
computeHash(); // 哈希计算耗时 1507毫秒
computeHash(); // 哈希计算耗时 2009毫秒
当只有一个线程可用时,操作现在是顺序进行的,就像在真正的单线程系统中一样。
Node.js的核心超能力:事件循环Node.js 能够用单线程处理多个操作的原因在于它的 事件循环机制。事件循环机制是一种架构模型,允许 Node.js 通过将耗时任务交给后台进程处理(如线程池或操作系统提供的服务)来执行异步操作。
当遇到阻塞操作时,Node.js 通过事件循环将任务交给后台处理。这使主线程可以继续处理其他任务,后台工作者则完成相应任务。一旦完成,结果会返回给事件循环进行处理。
Node.js的非阻塞(即非阻塞)特点Node.js 利用 非阻塞 I/O 处理并发。这意味着当遇到 I/O 请求(例如从数据库或文件读取)时,Node.js 不会因此停止工作。相反,它将任务交给操作系统并转而处理下一个请求。这使得 Node.js 可以高效处理数千个并发连接。
在执行文件访问、DNS 查询或加密等特定操作时,Node.js 使用 libuv 的线程池 来处理这些阻塞操作。线程池中的每个线程负责处理一个任务,任务完成后,结果会被送回事件循环中。
默认情况下,Node.js的线程池有四个线程,但你可以根据需要调整这个数量。
export UV_THREADPOOL_SIZE=8 # 设置UV线程池大小为8
对于处理许多阻塞操作(例如文件 I/O 或 CPU 密集型任务)的应用程序来说,增加线程池的大小特别有用。
Node.js 中的工作线程:真正的并行性当事件循环和线程池不足以应付时(特别是对于CPU密集型负载),Node.js引入了Worker Threads。这样你就可以在多个线程上并行执行JavaScript代码,真正地发挥了多核处理器的作用。
这里有一个使用Worker Threads的基本示例:
const { Worker } = require('worker_threads');
const worker = new Worker(`
const { parentPort } = require('worker_threads');
let count = 0;
for (let i = 0; i < 1e9; i++) {
count++;
}
parentPort.postMessage(count);
`, { eval: true });
worker.on('message', result => {
console.log(`来自 worker 的结果是: ${result}`);
});
此代码在一个单独的线程上运行耗时的计算,避免主线程被阻塞,保持应用程序的响应。
结论部分所以,Node.js是单线程还是多线程呢?这要看你关注的是架构的哪一部分。
- 单线程:Node.js 在一个线程中运行你的 JavaScript 代码(应用逻辑)。所有的事件处理器和回调函数都在这里执行。
- 多线程:对于非 JavaScript 的任务,如文件操作、加密和 DNS 查询,Node.js 将这些任务交给 libuv 管理的线程池处理。此外,Worker Threads 允许在必要时将 CPU 密集型的 JavaScript 任务运行在并行线程中。
这种混合方法使Node.js在处理I/O任务时非常高效,同时开发人员还可以在不阻塞主线程的情况下处理CPU密集型操作。
通过理解Node.js真正的运行时模型,你就能更好地优化你的应用,并充分利用异步编程的优势。
如果你觉得这篇博客不错,可以分享给可能会觉得它有用的朋友。可以关注我,了解更多类似的文章。
https://www.linkedin.com/in/itherohit/ (访问 LinkedIn 个人档案)
共同学习,写下你的评论
评论加载中...
作者其他优质文章