Nodejs 事件循环

Posted

技术标签:

【中文标题】Nodejs 事件循环【英文标题】:Nodejs Event Loop 【发布时间】:2012-05-27 16:17:53 【问题描述】:

nodejs 架构内部有两个事件循环吗?

libev/libuv v8 javascript 事件循环

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

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

是否有任何文档可以清楚地了解 nodejs 内部架构?

【问题讨论】:

【参考方案1】:

简单来说,Node 事件循环是架构级别的循环或循环,它帮助 Javascript 代码处理异步代码。

事件循环内部有不同的循环/循环,用于处理适当的工作,例如 setTimeouts、setimmediate、文件系统、网络请求、promise 和其他东西。

【讨论】:

【参考方案2】:

An Introduction to libuv

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。在node-v0.9.0版本的libuvlibev was removed。

还有一张 @BusyRich@@BusyRich 描述 Node.js 中的事件循环的图片


2017 年 5 月 9 日更新

根据此文档Node.js event loop,

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

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

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

阶段概述

计时器:此阶段执行由setTimeout()setInterval() 安排的回调。 I/O 回调:执行几乎所有的回调,除了 close 回调、定时器调度的回调和setImmediate()空闲,准备:只在内部使用。 poll:检索新的 I/O 事件;节点会在适当的时候在这里阻塞。 检查setImmediate() 回调在这里被调用。 关闭回调:例如socket.on('close', ...)

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

【讨论】:

您已经引用了“In the node-v0.9.0 version of libuv libev was removed”,但在 nodejs changelog 中没有关于它的描述。 github.com/nodejs/node/blob/master/CHANGELOG.md。如果 libev 被移除,那么现在如何在 nodejs 中执行异步 I/O? @intekhab,根据link,我认为基于libeio的libuv可以用作node.js中的事件循环。 @intekhab 我认为 libuv 正在实现与 I/O 和轮询相关的所有功能。在这里检查这个文档:docs.libuv.org/en/v1.x/loop.html 如果 Node.Js 应用程序收到请求。并且在这个请求中需要执行像 setTimeout(() => console.log('timeout'); , 10); 这样的代码setImmediate(()=> console.log('timeout'); ); console.log("Main") 那么nodeJs将如何将任务移动到定时器,检查,轮询阶段和热它将执行【参考方案3】:

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 在一个线程中一一运行所有指令。

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

【讨论】:

【参考方案4】:

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

事件循环是 JavaScript 编程模型的基本抽象概念。所以 V8 引擎为事件循环提供了一个默认实现,嵌入器(浏览器、节点)可以替换或扩展。你们可以找到事件循环的V8默认实现here NodeJS 中只有一个事件循环,由节点运行时提供。 V8 默认事件循环实现被 NodeJS 事件循环实现取代

【讨论】:

【参考方案5】:

我一直在亲自阅读node.js & v8的源码。

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

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

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

    LibEio 是一个异步执行输入输出的库。它处理文件描述符、数据处理程序、套接字等。您可以在此处阅读更多信息here。

    LibUv 是 libeio、libev、c-ares(用于 DNS)和 iocp(用于 windows 异步 io)之上的抽象层。 LibUv 执行、维护和管理事件池中的所有 io 和事件。 (在 libeio 线程池的情况下)。你应该在 libUv 上查看Ryan Dahl's tutorial。这将使您开始更了解 libUv 本身的工作原理,然后您将了解 node.js 如何在 libuv 和 v8 之上工作。

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

JS-conference JSConf2011 ( has very irritative sfx) Understanding event driven programming Understanding the node.js event loop

要了解 libeio 如何与 node.js 一起使用以创建异步模块,您应该查看 this example。

基本上在 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 是请求处理程序,它可以是一个结构/对象,其目的是提供线程池和主线程之间的通信。

【讨论】:

依靠libuv内部使用libev的事实是让你的代码不跨平台的好方法。你应该只关心 libuv 的公共接口。 @Raynos libuv 旨在确保其 x-platfousing 多个库。正确的 ?因此使用 libuv 是个好主意 @Abhishek From Doc process.nextTick - 在事件循环的下一个循环中调用此回调。这不是 setTimeout(fn, 0) 的简单别名,它更有效。这是指哪个事件循环? V8 事件循环? 注意libuv是no longer implemented on top of libev。 有没有办法“看到”这个事件呢?我希望能够看到堆栈上的调用顺序并看到新函数被推送到那里以更好地了解正在发生的事情......是否有一些变量可以告诉你什么被推送到事件队列?【参考方案6】:

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

让我试着在一个抽象的例子的帮助下解释事件驱动模型的工作原理,在抽象的 UNIX 环境中,在 Node 的上下文中,截至今天。

计划的观点:

脚本引擎开始执行脚本。 任何时候遇到 CPU 绑定操作,都会完整地内联(真实机器)执行。 每当遇到 I/O 绑定操作时,请求及其完成处理程序都会注册到“事件机制”(虚拟机) 以与上述相同的方式重复操作,直到脚本结束。 CPU 绑定操作 - 执行内联、I/O 绑定操作,向机器发出请求,如上所述。 当 I/O 完成时,回调监听器。

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

节点视角:

有一个线程来托管运行时。 拿起用户脚本。 编译成原生 [lever v8] 加载二进制文件,然后跳转到入口点。 编译后的代码使用编程原语在线执行 CPU 绑定活动。 许多与 I/O 和定时器相关的代码都有本地包装。例如,网络 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 环境变量配置)对文件进行操作,而事件驱动的抽象则完好无损。

希望这会有所帮助。

【讨论】:

你是怎么知道这些东西的,能指点一下出处吗?【参考方案7】:

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

【讨论】:

【参考方案8】:

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

Node.js 事件循环模型

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

Node.js 将工作添加到事件队列中,然后让运行事件循环的单个线程将其拾取。事件循环抓取事件队列中的顶部项,执行它,然后抓取下一项。

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

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

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

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

【讨论】:

以上是关于Nodejs 事件循环的主要内容,如果未能解决你的问题,请参考以下文章

nodejs 事件循环

浏览器事件循环与node事件循环

js循环点击div事件

小痴Nodejs升级路---事件循环eventloop

在nodejs中事件循环分析

js如何循环添加点击事件