nodejs架构内部是否有两个事件循环?
- libev/libuv
- v8 javascript 事件循环
在 I/O 请求上,节点是否将请求排队到 libeio,然后使用 libev 通过事件通知数据的可用性,最后这些事件由 v8 事件循环使用回调处理?
基本上,libev 和 libeio 是如何集成到 nodejs 架构中的?
是否有任何文档可以清楚地了解 nodejs 内部架构?
nodejs架构内部是否有两个事件循环?
在 I/O 请求上,节点是否将请求排队到 libeio,然后使用 libev 通过事件通知数据的可用性,最后这些事件由 v8 事件循环使用回调处理?
基本上,libev 和 libeio 是如何集成到 nodejs 架构中的?
是否有任何文档可以清楚地了解 nodejs 内部架构?
我一直在亲自阅读node.js & v8的源码。
当我试图了解 node.js 架构以编写本机模块时,我遇到了类似的问题。
我在这里发布的是我对 node.js 的理解,这也可能有点偏离轨道。
Libev是在 node.js 内部实际运行的事件循环,用于执行简单的事件循环操作。它最初是为 *nix 系统编写的。Libev 为进程运行提供了一个简单但优化的事件循环。您可以在此处阅读有关 libev的更多信息。
LibUv是 libeio、libev、c-ares(用于 DNS)和 iocp(用于 windows asynchronous-io)之上的抽象层。LibUv 执行、维护和管理事件池中的所有 io 和事件。(在 libeio 线程池的情况下)。您应该查看Ryan Dahl关于 libUv 的教程。这将使您对 libUv 本身的工作方式变得更有意义,然后您将了解 node.js 如何在 libuv 和 v8 之上工作。
要了解 javascript 事件循环,您应该考虑观看这些视频
要了解 libeio 如何与 node.js 一起使用以创建异步模块,您应该查看这个示例。
基本上在 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
请求处理程序可以是一个结构/对象,其动机是提供线程池和主线程之间的通信。
看起来一些讨论的实体(例如:libev 等)已经失去了相关性,因为它已经有一段时间了,但我认为这个问题仍然有很大的潜力。
让我试着借助一个抽象的例子来解释事件驱动模型的工作原理,在抽象的 UNIX 环境中,在 Node 的上下文中,截至今天。
节目视角:
上面的事件机制称为 libuv AKA 事件循环框架。Node 利用这个库来实现它的事件驱动编程模型。
节点视角:
虽然以这种方式满足了大多数功能,但文件操作的一些(异步版本)是在附加线程的帮助下执行的,这些线程很好地集成到 libuv 中。虽然网络 I/O 操作可以等待外部事件,例如另一个端点响应数据等,但文件操作需要节点本身的一些工作。例如,如果您打开一个文件并等待 fd 准备好数据,这不会发生,因为实际上没有人在读取!同时,如果您在主线程中内联读取文件,它可能会阻塞程序中的其他活动,并且可能会导致明显的问题,因为与 cpu 绑定活动相比,文件操作非常慢。因此使用内部工作线程(可通过 UV_THREADPOOL_SIZE 环境变量配置)对文件进行操作,
希望这可以帮助。
node.js项目始于 2009 年,是一个与浏览器分离的 JavaScript 环境。使用 Google 的V8和 Marc Lehmann 的libev,node.js 将 I/O 模型(事件)与一种非常适合编程风格的语言结合在一起;由于它被浏览器塑造的方式。随着 node.js 越来越流行,让它在 Windows 上运行很重要,但 libev 只在 Unix 上运行。Windows 等效的内核事件通知机制(如 kqueue 或 (e)poll)是 IOCP。libuv 是围绕 libev 或 IOCP 的抽象,具体取决于平台,为用户提供基于 libev 的 API。在 libuv 的 node-v0.9.0 版本中,libev 被删除。
还有一张@BusyRich描述 Node.js 中的事件循环的图片
2017 年 5 月 9 日更新
根据这个文档Node.js 事件循环,
下图显示了事件循环操作顺序的简化概览。
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
注意:每个框将被称为事件循环的“阶段”。
阶段概述
setTimeout()
:此阶段执行由and安排的回调setInterval()
。setImmediate()
.setImmediate()
回调在这里被调用。socket.on('close', ...)
。在事件循环的每次运行之间,Node.js 检查它是否正在等待任何异步 I/O 或计时器,如果没有则干净地关闭。
NodeJs 架构中有一个事件循环。
节点应用程序在单线程事件驱动模型中运行。但是,Node 在后台实现了一个线程池,以便可以执行工作。
Node.js 将工作添加到事件队列中,然后让运行事件循环的单个线程将其拾取。事件循环抓取事件队列中的顶部项,执行它,然后抓取下一项。
当执行更长生命周期或阻塞 I/O 的代码时,它不会直接调用函数,而是将函数添加到事件队列中,并在函数完成后执行回调。当 Node.js 事件队列上的所有事件都已执行时,Node.js 应用程序将终止。
当我们的应用程序函数阻塞 I/O 时,事件循环开始遇到问题。
Node.js 使用事件回调来避免等待阻塞 I/O。因此,任何执行阻塞 I/O 的请求都在后台的不同线程上执行。
当从事件队列中检索到阻塞 I/O 的事件时,Node.js 从线程池中检索一个线程,并在那里而不是在主事件循环线程上执行函数。这可以防止阻塞 I/O 阻止事件队列中的其余事件。
libuv 只提供了一个事件循环,V8 只是一个 JS 运行时引擎。
作为一个 javascript 初学者,我也有同样的疑问,NodeJS 是否包含 2 个事件循环?经过长时间的研究和与 V8 贡献者之一的讨论,我得到了以下概念。
该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 代码处理异步代码。
事件循环内部有不同的循环/循环,用于处理适当的工作,例如 setTimeouts、setimmediate、文件系统、网络请求、promise 和其他东西。