Nodejs 事件循环

在 nodejs 体系结构中是否存在两个内部事件循环?

  • 利比夫/利布夫
  • V8 javascript 事件循环

在 I/O 请求中,节点是否将请求排队到 libeio,libeio 通过使用 libev 的事件通知数据的可用性,最后通过使用回调的 v8事件循环处理这些事件?

基本上,libev 和 libeio 是如何整合到 nodejs 架构中的?

是否有任何可用的文档来提供节点内部架构的清晰图片?

30342 次浏览

我一直在亲自阅读 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 different-io)之上的抽象层。LibUv 执行、维护和管理事件池中的所有 io 和事件。(在 libeio 线程池的情况下)。你应该看看 libUv 上的 Ryan Dahl 的教程。这将使您了解 libUv 是如何工作的,然后您将了解 node.js 是如何在 libUv 和 v8之上工作的。

为了理解 javascript 事件循环,你应该考虑观看这些视频

要了解 libeio 是如何与 node.js 一起使用以创建异步模块的,应该查看 这个例子

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

例如:

eio_custom(Task,FLAG,AfterTask,Eio_REQUEST);

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

NodeJs 体系结构中有一个事件循环。

Js 事件循环模型

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

Js 将工作添加到事件队列中,然后让一个线程运行一个事件循环来提取它。事件循环获取事件队列中的顶部项,执行它,然后获取下一个项。

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

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

Js 使用事件回调来避免等待阻塞 I/O。因此,执行阻塞 I/O 的任何请求都在后台的另一个线程上执行。

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

利布夫概论

Node Js项目始于2009年,当时是一个 JavaScript 环境,与浏览器分离开来。使用 Google 的 V8引擎和 Marc Lehmann 的 Libev,node.js 将 I/O-event 模型与一种非常适合编程风格的语言结合起来,这是由浏览器塑造的。随着 node.js 越来越流行,让它在 Windows 上工作很重要,但 libev 只能在 Unix 上运行。内核事件通知机制(如 kqueue 或(e) poll)的 Windows 等价物是 IOCP。Libuv 是一个围绕 libev 或 IOCP 的抽象,取决于平台,为用户提供基于 libev 的 API。在 libuv 利贝夫被移除了的 node-v0.9.0版本中。

还有一张由@大忙人描述 Node.js 中的事件循环的图片


更新05/09/2017

根据 Js 事件循环医生的说法,

下图显示了事件循环操作顺序的简化概述。

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

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

阶段概览

  • Timers : 此阶段执行由 setTimeout()setInterval()调度的回调。
  • I/O 回调 : 除了 激烈的复试、计时器调度的回调和 setImmediate()之外,执行几乎所有的回调。
  • 闲置,准备 : 仅在内部使用。
  • Poll : 检索新的 I/O 事件; 适当时节点将在此阻塞。
  • Check : 这里调用 setImmediate()回调函数。
  • 关闭回调 : 例如 socket.on('close', ...)

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

Libuv 只提供了一个事件循环,V8只是一个 JS 运行时引擎。

看起来一些被讨论的实体(例如: libev 等)已经失去了相关性,由于事实上已经有一段时间了,但我认为这个问题仍然有很大的潜力。

让我尝试借助一个抽象示例来解释事件驱动模型的工作原理,这个示例是在一个抽象的 UNIX 环境中,在 Node 的上下文中,从今天开始。

程序视角:

  • 脚本引擎开始执行脚本。
  • 任何时候遇到 CPU 绑定操作时,都会在其完整性中内联(实际机器)执行该操作。
  • 任何时候遇到 I/O 绑定操作时,请求及其完成处理程序都会向“事件机器”(虚拟机)注册
  • 以上面相同的方式重复操作,直到脚本结束。CPU 绑定操作-在线执行,I/O 绑定操作,对机器的请求如上。
  • 当 I/O 完成时,侦听器被回调。

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

Node 的观点:

  • 有一个线程来承载运行库。
  • 拿起用户脚本。
  • 将它编译成原生的[杠杆 v8]
  • 加载二进制文件,然后跳入入口点。
  • 编译后的代码使用编程原语以内联方式执行 CPU 绑定活动。
  • 许多与 I/O 和计时器相关的代码都有本机包装。
  • 因此,I/O 调用从脚本路由到 C + + 桥接器,使用 I/O 句柄和完成处理程序作为参数传递。
  • 本机代码执行 libuv 循环。它获取循环,将表示 I/O 的低级事件排队,并将本机回调包装器放入 libuv 循环结构中。
  • 本机代码返回到脚本-目前没有发生 I/O!
  • 上面的代码重复了很多次,直到所有的非 I/O 代码都被执行,所有的 I/O 代码都被注册为 libuv。
  • 最后,当系统中没有可执行的内容时,节点将控制传递给 libuv
  • Libuv 开始行动,它获取所有已注册的事件,查询操作系统以获得它们的可操作性。
  • 那些已经准备好在非阻塞模式下进行 I/O 的,将被拾取、执行 I/O 并发出它们的回调。一个接一个。
  • 那些还没有准备好的(例如套接字读取,其他端点还没有编写任何东西)将继续与操作系统探测,直到它们可用。
  • 循环内部维护一个不断增加的计时器。当应用程序请求延迟回调(例如 setTimeout)时,将利用这个内部计时器值来计算触发回调的合适时间。

虽然大多数功能都是以这种方式满足的,但是一些(异步版本)的文件操作是在附加线程的帮助下完成的,并且很好地集成到 libuv 中。虽然网络 I/O 操作可以等待外部事件,如其他端点响应数据等,但是文件操作需要从节点本身做一些工作。例如,如果您打开一个文件并等待 fd 准备好数据,这种情况不会发生,因为实际上没有人正在读取!同时,如果您在主线程中内联地读取文件,它可能会阻塞程序中的其他活动,并且可能产生可见的问题,因为与 CPU 绑定的活动相比,文件操作非常慢。因此,内部工作线程(可以通过 UV _ THREADPOOL _ SIZE 环境变量配置)被用来对文件进行操作,而事件驱动的抽象从程序的角度来看是完整的。

希望这个能帮上忙。

作为一个 javascript 初学者,我也有同样的疑问,NodeJS 是否包含2个事件循环?.经过长时间的研究和与 V8贡献者之一的讨论,我得到了以下概念。

  • 事件循环是 JavaScript 编程模型的基本抽象概念。因此,V8引擎为事件循环 哪些嵌入程序(浏览器、节点)可以替换或扩展 提供了默认实现。您可以找到事件循环 给你的 V8默认实现
  • 在 NodeJS 中,只存在一个事件循环 ,该循环由节点运行时提供。V8缺省事件循环实现被 NodeJS 事件循环实现替换

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 还使用其他线程来执行一些计算开销很大的任务。

如果事件池负责执行计算代价高昂的任务,那么 Node 应用程序就无法执行其他任务。

我们的 CPU 在一个线程中运行所有的指令。

通过使用线程池,我们可以在进行计算时在事件循环中做其他事情。

简单地说,Node 事件循环是体系结构层次上的循环,它帮助 Javascript 代码处理异步代码。

事件循环内部有不同的循环/循环,用于处理适当的作业,例如 setTimeout、 setimmediate、文件系统、网络请求、承诺和其他内容。