Nodejs事件循环


141

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

  • libev / libuv
  • v8 javascript事件循环

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

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

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

Answers:


175

我一直在亲自阅读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 的教程。这将使您对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请求处理程序可以是结构/对象,其目的是在线程池和主线程之间提供通信。


依靠libuv内部使用libev的事实是使您的代码不跨平台的好方法。您应该只关心libuv的公共接口。
雷诺斯2012年

1
@Raynos libuv旨在确保其x-platfousing多个库。对 ?因此,使用libuv是个好主意
ShrekOverflow 2012年

1
@Abhishek From Doc- process.nextTick在事件循环的下一个循环中,调用此回调。这不是setTimeout(fn,0)的简单别名,它的效率要高得多。这指的是哪个事件循环?V8事件循环?
泰米尔语

2
请注意,libuv 不再在libev之上实现
strcat

4
有没有办法“看到”此事件?我希望能够看到堆栈上的调用顺序,并看到新的函数被推送到那里以更好地了解发生了什么...是否有一些变量告诉您​​什么被推送到了事件队列?
tbarbe

20

似乎已经讨论了一些实体(例如:libev等),因为已经有一段时间了,但我认为这个问题仍然具有很大的潜力。

到目前为止,让我尝试在抽象的UNIX环境中,在Node的上下文中,借助抽象示例来解释事件驱动模型的工作。

程序的观点:

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

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

节点的观点:

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

尽管大多数功能都是以这种方式提供的,但某些文件操作(异步版本)是在附加线程的帮助下进行的,这些线程已很好地集成到libuv中。尽管网络I / O操作可以等待外部事件(例如,另一个终结点用数据进行响应)的等待,但是文件操作需要节点本身进行一些工作。例如,如果您打开一个文件并等待fd准备好数据,它将不会发生,因为实际上没有人在读!同时,如果您在主线程中内联读取文件,它可能会阻止程序中的其他活动,并且可能会引起明显的问题,因为与cpu绑定的活动相比,文件操作非常慢。因此,使用内部工作线程(可通过UV_THREADPOOL_SIZE环境变量配置)来对文件进行操作,

希望这可以帮助。


您怎么知道这些事,您能指出我的出处吗?
Suraj Jain

18

libuv简介

Node.js的项目于2009年开始为JavaScript环境从浏览器分离。node.js 使用Google的V8和Marc Lehmann的libev,将事件化的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中的事件循环


更新05/09/2017

根据此文档Node.js事件循环

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

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

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

阶段概述

  • 计时器:此阶段执行由setTimeout()和安排的回调setInterval()
  • I / O回调:执行几乎所有的回调,但close回调,计时器安排的回调setImmediate()
  • 空闲,准备:仅在内部使用。
  • poll:检索新的I / O事件;适当时,节点将在此处阻塞。
  • checksetImmediate()在这里调用回调。
  • 关闭回调:例如socket.on('close', ...)

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


您已经引用了“ In the node-v0.9.0 version of libuv libev was removed”,但是在nodejs中没有关于它的描述changeloggithub.com/nodejs/node/blob/master/CHANGELOG.md。而且,如果删除了libev,那么现在如何在nodejs中执行异步I / O呢?
intekhab

@intekhab,通过此链接,我认为基于libeio的libuv可以用作node.js中的事件循环。
zangw

@intekhab我认为libuv正在实现与I / O和轮询相关的所有功能。在这里检查此文档:docs.libuv.org/en/v1.x/loop.html
mohit kaushik

13

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阻止事件队列中的其余事件。


8

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


1

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

  • 事件循环是JavaScript编程模型的基本抽象概念。因此,V8引擎为事件循环提供了默认实现,嵌入器(浏览器,节点)可以替换或扩展。你们可以在这里找到事件循环的V8默认实现
  • 在NodeJS中,仅存在一个事件循环,该循环由节点运行时提供。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还使用其他线程来执行一些计算量大的任务。

如果事件池负责执行计算量大的任务,那么我们的Node应用程序将无能为力。

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

通过使用线程池,我们可以在进行计算时在事件循环内执行其他操作。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.