继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Node.js 并不是单线程的,它使用了事件循环(Event Loop)机制来处理并发任务

米琪卡哇伊
关注TA
已关注
手记 228
粉丝 4
获赞 30

Node.js是单线程吗?

是的 和 不是。是的,它的主线程确实是单线程的,因为 JavaScript 本身就是在单线程上运行的。你编写的所有代码都在这个主线程上运行,这也驱动了著名的事件循环。然而,误解在于认为 Node.js 所做的一切都只是在这个单一的主线程上运行。实际上,Node.js 的很大一部分是多线程的,但这发生在 JavaScript 直接执行的范围之外。

V8 和 libuv

Node.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 个人档案)

https://itherohit.dev/

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP