Node.js事件循环

147

在Node.js的架构中,是否存在两个内部事件循环?

  • libev/libuv
  • v8 JavaScript事件循环

当进行I/O请求时,Node.js会将请求排队到libeio中,然后使用libev通过事件通知数据可用性,最后这些事件使用回调由v8事件循环处理?

基本上,libev和libeio如何与Node.js架构集成?

是否有文档可提供有关Node.js内部架构的清晰图像?

8个回答

180

我个人一直在阅读node.js和v8的源代码。

当我试图理解node.js架构以编写本地模块时,我遇到了与您类似的问题。

我在此发布的是我对node.js的理解,这可能有点偏离轨道。

  1. Libev 是事件循环,在node.js内部实际运行以执行简单的事件循环操作。它最初是为*nix系统编写的。Libev提供了一个简单而优化的事件循环,使进程可以运行。您可以在这里了解更多关于libev的信息。

  2. LibEio 是用于异步执行输入输出的库。它处理文件描述符、数据处理程序、套接字等。您可以在这里了解更多关于它的信息。

  3. LibUv 是在libeio、libev、c-ares(用于DNS)和iocp(用于Windows异步IO)之上的抽象层。LibUv执行、维护和管理事件池中的所有IO和事件。(在libeio线程池的情况下)。您应该查看Ryan Dahl的教程,这将让你更了解libUv如何工作,然后你将了解node.js如何在libuv和v8的顶部运行。

要理解JavaScript事件循环,您应该观看这些视频

要查看如何在node.js中使用libeio创建异步模块,您应该查看此示例

基本上,在node.js内部发生的情况是v8循环运行并处理所有JavaScript部分以及C++模块[当它们在主线程中运行时(根据官方文档,node.js本身是单线程的)]。当在主线程之外时,libev和libeio在线程池中处理它,并且libev提供与主循环的交互。因此,据我理解,node.js有1个永久事件循环:那就是v8事件循环。为了处理C++异步任务,它使用线程池[通过libeio和libev]。

例如:

eio_custom(Task,FLAG,AfterTask,Eio_REQUEST);

在所有模块中出现的通常是在线程池中调用函数Task。当它完成时,它会在主线程中调用AfterTask函数。而Eio_REQUEST则是请求处理程序,可以是一个提供线程池和主线程之间通信的结构/对象。


1
@Raynos 的 libuv 旨在确保其跨平台性,使用多个库。因此,使用 libuv 是一个好主意。 - ShrekOverflow
1
@Abhishek 来自文档 process.nextTick - 在下一次事件循环中调用此回调。这不是一个简单的setTimeout(fn, 0)别名,它更有效率。这指的是哪个事件循环?V8事件循环吗? - Tamil
2
请注意,libuv不再在libev之上实现。 - strcat
那时候是开心的 :-) 是的,我认为我应该更新帖子。 - ShrekOverflow
5
有没有一种方法可以“查看”这个事件队列?我想要能够看到调用堆栈的顺序,并且看到新的函数被推入其中,以更好地理解发生了什么......是否有一些变量可以告诉你已经被推入事件队列的内容? - tbarbe
显示剩余6条评论

23

看起来一些讨论的实体(例如:libev等)已经失去了影响力,因为它已经有一段时间了,但我认为这个问题仍然有巨大的潜力。

让我尝试用一个抽象的例子,在一个抽象的UNIX环境中,以Node的上下文,解释事件驱动模型的工作原理。

程序的角度:

  • 脚本引擎开始执行脚本。
  • 每当遇到一个CPU绑定操作时,它会在其完整性内联执行(真实机器)。
  • 每当遇到I/O绑定操作时,请求及其完成处理程序将在'事件机制'(虚拟机)注册。
  • 以同样的方式重复以上操作,直到脚本结束。CPU绑定操作-内联执行,I/O绑定操作,则像上面那样请求机器。
  • 当I/O完成时,回调监听器。

上述事件机制被称为libuv AKA事件循环框架。 Node利用此库实现其事件驱动编程模型。

Node的角度:

  • 有一个线程来托管运行时。
  • 接收用户脚本。
  • 将其编译成本地代码[利用v8]
  • 加载二进制文件,并跳转到入口点。
  • 编译后的代码使用编程原语内联执行CPU绑定活动。
  • 许多I/O和计时器相关的代码有本机包装。例如,网络I/O。
  • 因此,I/O调用从脚本路由到C++桥接程序,并将I/O句柄和完成处理程序作为参数传递。
  • 本机代码操作libuv循环。 它获取循环,将代表I/O的低级事件和本机回调包装器排队到libuv循环结构中。
  • 本机代码返回到脚本-此时不会发生I/O!
  • 重复上述项目多次,直到所有非I/O代码都被执行,并且所有I/O代码都已注册到libuv。
  • 最后,当系统没有其他任务要执行时,Node.js将控制权交给libuv。
  • libuv开始工作,它会查找所有已注册的事件,并查询操作系统以获取它们是否可用。
  • 那些以非阻塞模式准备好进行I/O的事件将被选中进行I/O操作,并依次调用它们的回调函数。
  • 那些还没有准备好的事件(例如套接字读取,等待另一端点尚未写入)将继续通过操作系统进行探测,直到它们准备就绪。
  • 事件循环内部保持着一个不断增长的计时器。当应用程序请求延迟回调(例如setTimeout),该内部计时器值将用于计算触发回调的正确时间。
  • 虽然大多数功能都是以这种方式处理的,但某些文件操作的异步版本需要使用额外的线程来完成,并与libuv完美集成。虽然网络I/O操作可以等待外部事件(例如另一端点响应数据等),但文件操作需要一些来自Node.js本身的工作。例如,如果您打开一个文件并等待fd准备好数据,它是不会发生的,因为实际上没有人在读取!同时,如果您在主线程中直接从文件中读取,它可能会阻塞程序中的其他活动,并可能会出现可见问题,因为文件操作与CPU绑定的活动相比非常缓慢。因此,在保持程序的事件驱动抽象视角不变的同时,使用内部工作线程(可以通过UV_THREADPOOL_SIZE环境变量进行配置)来操作文件。

    希望这有所帮助。


    你是怎么知道这些的,能告诉我来源吗? - Suraj Jain

    19

    libuv介绍

    Node.js项目始于2009年,作为一个脱离浏览器的JavaScript环境。使用了Google的V8和Marc Lehmann的libev,Node.js将I/O模型(事件驱动)与适合这种编程风格的语言结合在一起,因为它是由浏览器塑造出来的。随着Node.js越来越流行,使其能够在Windows上运行变得重要,但libev只能在Unix上运行。与平台相关的IOCP是类似于kqueue或(e)poll的内核事件通知机制。libuv是围绕libev或IOCP的抽象,为用户提供基于libev的API。在node-v0.9.0版本的libuv中,移除了libev

    此外,还有一个图片描述了Node.js中的事件循环,由@BusyRich绘制:


    更新于05/09/2017

    根据Node.js事件循环文档,下图显示了事件循环的简化操作顺序。

       ┌───────────────────────┐
    ┌─>│        timers         │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    │  │     I/O callbacks     │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    │  │     idle, prepare     │
    │  └──────────┬────────────┘      ┌───────────────┐
    │  ┌──────────┴────────────┐      │   incoming:   │
    │  │         poll          │<─────┤  connections, │
    │  └──────────┬────────────┘      │   data, etc.  │
    │  ┌──────────┴────────────┐      └───────────────┘
    │  │        check          │
    │  └──────────┬────────────┘
    │  ┌──────────┴────────────┐
    └──┤    close callbacks    │
       └───────────────────────┘
    

    注意:每个方框都将被称为事件循环的“阶段”。

    阶段概述

    • 计时器:此阶段执行由 setTimeout()setInterval() 调度的回调函数。
    • I/O 回调:执行几乎所有回调,但不包括关闭回调、由计时器调度的回调和 setImmediate()
    • 空闲,准备:仅在内部使用。
    • 轮询:检索新的 I/O 事件;在适当时,Node.js 将在此处阻塞。
    • 检查setImmediate() 回调在此处调用。
    • 关闭回调:例如 socket.on('close', ...)

    在事件循环的每次运行之间,Node.js 检查是否正在等待任何异步 I/O 或计时器,并在没有任何等待时干净地关闭。


    你引用了"在node-v0.9.0版本的libuv中删除了libev",但是在nodejs的changelog中没有关于此的描述。 https://github.com/nodejs/node/blob/master/CHANGELOG.md.如果libev被移除了,那么现在nodejs是如何执行异步I/O操作的? - intekhab
    @intekhab,根据这个链接,我认为基于libeio的libuv可以用作node.js中的事件循环。 - zangw
    @intekhab 我认为libuv正在实现所有与I/O和轮询相关的功能。在这个文档中查看:http://docs.libuv.org/en/v1.x/loop.html - mohit kaushik
    如果Node.Js应用程序收到请求。并且在此请求中需要执行以下代码:setTimeout(() => { console.log('timeout'); }, 10);setImmediate(()=> { console.log('timeout'); });console.log("Main")那么NodeJs将如何将任务移动到计时器、检查、轮询阶段,并执行它? - jitendra rajput

    13

    NodeJs的架构中有一个事件循环。

    Node.js事件循环模型

    Node应用程序以单线程事件驱动模型运行。但是,Node在后台实现了一个线程池,以便可以执行工作。

    Node.js将工作添加到事件队列中,然后由单个线程运行事件循环来接收它。事件循环获取事件队列中的顶部项目,执行它,然后获取下一个项目。

    当执行生命周期较长或具有阻塞I/O的代码时,Node.js不会直接调用函数,而是将函数添加到事件队列中,并附加一个回调函数,在函数完成后执行该回调函数。 当Node.js事件队列中的所有事件都被执行后,Node.js应用程序终止。

    当我们的应用程序功能在I/O上阻塞时,事件循环开始遇到问题。

    Node.js使用事件回调来避免等待阻塞I/O。因此,执行任何执行阻塞I/O的请求都在后台的不同线程上执行。

    当从事件队列中检索到一个阻塞I/O的事件时,Node.js从线程池中检索一个线程,并在那里执行该函数,而不是在主事件循环线程上执行。这样可以防止阻塞I/O阻塞事件队列中的其他事件。


    8

    libuv只提供一个事件循环,而V8仅是一个JS运行时引擎。


    2
    作为一个 JavaScript 初学者,我也曾经有过同样的疑惑,NodeJS 是否包含两个事件循环呢?经过长时间的研究和与 V8 贡献者之一的讨论,我得到了以下概念:
    • 事件循环是 JavaScript 编程模型的基本抽象概念。因此,V8 引擎提供了事件循环的默认实现,嵌入式程序(浏览器、Node)可以替换或扩展。您可以在这里找到 V8 默认事件循环的实现 here
    • 在 NodeJS 中,只存在一个事件循环,由 Node 运行时提供。V8 默认的事件循环实现被 NodeJS 的事件循环实现所取代。

    0

    pbkdf2 函数有 JavaScript 实现,但实际上它会把所有工作委托给 C++ 端完成。

    env->SetMethod(target, "pbkdf2", PBKDF2);
      env->SetMethod(target, "generateKeyPairRSA", GenerateKeyPairRSA);
      env->SetMethod(target, "generateKeyPairDSA", GenerateKeyPairDSA);
      env->SetMethod(target, "generateKeyPairEC", GenerateKeyPairEC);
      NODE_DEFINE_CONSTANT(target, OPENSSL_EC_NAMED_CURVE);
      NODE_DEFINE_CONSTANT(target, OPENSSL_EC_EXPLICIT_CURVE);
      NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS1);
      NODE_DEFINE_CONSTANT(target, kKeyEncodingPKCS8);
      NODE_DEFINE_CONSTANT(target, kKeyEncodingSPKI);
      NODE_DEFINE_CONSTANT(target, kKeyEncodingSEC1);
      NODE_DEFINE_CONSTANT(target, kKeyFormatDER);
      NODE_DEFINE_CONSTANT(target, kKeyFormatPEM);
      NODE_DEFINE_CONSTANT(target, kKeyTypeSecret);
      NODE_DEFINE_CONSTANT(target, kKeyTypePublic);
      NODE_DEFINE_CONSTANT(target, kKeyTypePrivate);
      env->SetMethod(target, "randomBytes", RandomBytes);
      env->SetMethodNoSideEffect(target, "timingSafeEqual", TimingSafeEqual);
      env->SetMethodNoSideEffect(target, "getSSLCiphers", GetSSLCiphers);
      env->SetMethodNoSideEffect(target, "getCiphers", GetCiphers);
      env->SetMethodNoSideEffect(target, "getHashes", GetHashes);
      env->SetMethodNoSideEffect(target, "getCurves", GetCurves);
      env->SetMethod(target, "publicEncrypt",
                     PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
                                             EVP_PKEY_encrypt_init,
                                             EVP_PKEY_encrypt>);
      env->SetMethod(target, "privateDecrypt",
                     PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
                                             EVP_PKEY_decrypt_init,
                                             EVP_PKEY_decrypt>);
      env->SetMethod(target, "privateEncrypt",
                     PublicKeyCipher::Cipher<PublicKeyCipher::kPrivate,
                                             EVP_PKEY_sign_init,
                                             EVP_PKEY_sign>);
      env->SetMethod(target, "publicDecrypt",
                     PublicKeyCipher::Cipher<PublicKeyCipher::kPublic,
                                             EVP_PKEY_verify_recover_init,
                                             EVP_PKEY_verify_recover>);
    

    资源:https://github.com/nodejs/node/blob/master/src/node_crypto.cc

    Libuv模块还有另一个职责,与标准库中某些非常特定的函数相关。

    对于一些标准库函数调用,Node C++端和Libuv决定在事件循环之外进行昂贵的计算。

    相反,它们利用了一种称为线程池的东西,线程池是一系列四个线程,可用于运行计算密集型任务,例如pbkdf2函数。

    默认情况下,Libuv在此线程池中创建4个线程。

    除了事件循环中使用的线程外,还有其他四个线程可用于卸载需要在我们的应用程序内部发生的昂贵计算。

    Node标准库中包含的许多函数自动利用此线程池。其中之一就是pbkdf2函数。

    这个线程池的存在非常重要。

    因此,Node并不是真正的单线程,因为有其他线程可以用于执行一些计算密集型任务。

    如果事件池负责执行计算密集型任务,那么我们的Node应用程序就无法做其他事情。
    我们的CPU逐个线程运行所有指令。
    通过使用线程池,我们可以在计算发生时在事件循环中执行其他操作。

    0
    简单来说,Node事件循环是一种在架构层面上的循环或轮询,它帮助JavaScript代码处理异步代码。
    事件循环内部有不同的循环/周期,用于处理适当的工作,例如setTimeouts、setimmediate、文件系统、网络请求、承诺和其他内容。

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接