彻底搞懂nodejs事件循环

nodejs是单线程执行的,同时它又是基于事件驱动的非阻塞IO编程模型。这就使得我们不用等待异步操作结果返回,就可以继续往下执行代码。当异步事件触发之后,就会通知主线程,主线程执行相应事件的回调。

以上是众所周知的内容。今天我们从源码入手,分析一下nodejs的事件循环机制。

nodejs架构

首先,我们先看下nodejs架构,下图所示:

如上图所示,nodejs自上而下分为

  • 用户代码 ( js 代码 )

用户代码即我们编写的应用程序代码、npm包、nodejs内置的js模块等,我们日常工作中的大部分时间都是编写这个层面的代码。

  • binding代码或者三方插件(js 或 C/C++ 代码)

胶水代码,能够让js调用C/C++的代码。可以将其理解为一个桥,桥这头是js,桥那头是C/C++,通过这个桥可以让js调用C/C++。
在nodejs里,胶水代码的主要作用是把nodejs底层实现的C/C++库暴露给js环境。
三方插件是我们自己实现的C/C++库,同时需要我们自己实现胶水代码,将js和C/C++进行桥接。

  • 底层库

nodejs的依赖库,包括大名鼎鼎的V8、libuv。
V8: 我们都知道,是google开发的一套高效javascript运行时,nodejs能够高效执行 js 代码的很大原因主要在它。
libuv:是用C语言实现的一套异步功能库,nodejs高效的异步编程模型很大程度上归功于libuv的实现,而libuv则是我们今天重点要分析的。
还有一些其他的依赖库
http-parser:负责解析http响应
openssl:加解密
c-ares:dns解析
npm:nodejs包管理器

关于nodejs不再过多介绍,大家可以自行查阅学习,接下来我们重点要分析的就是libuv。

libuv 架构

我们知道,nodejs实现异步机制的核心便是libuv,libuv承担着nodejs与文件、网络等异步任务的沟通桥梁,下面这张图让我们对libuv有个大概的印象:

这是libuv官网的一张图,很明显,nodejs的网络I/O、文件I/O、DNS操作、还有一些用户代码都是在 libuv 工作的。
既然谈到了异步,那么我们首先归纳下nodejs里的异步事件:

  • 非I/O:
    • 定时器(setTimeout,setInterval)
    • microtask(promise)
    • process.nextTick
    • setImmediate
    • DNS.lookup
  • I/O:
    • 网络I/O
    • 文件I/O
    • 一些DNS操作

网络I/O

对于网络I/O,各个平台的实现机制不一样,linux 是 epoll 模型,类 unix 是 kquene 、windows 下是高效的 IOCP 完成端口、SunOs 是 event ports,libuv 对这几种网络I/O模型进行了封装。

文件I/O、异步DNS操作

libuv内部还维护着一个默认4个线程的线程池,这些线程负责执行文件I/O操作、DNS操作、用户异步代码。当 js 层传递给 libuv 一个操作任务时,libuv 会把这个任务加到队列中。之后分两种情况:

  • 1、线程池中的线程都被占用的时候,队列中任务就要进行排队等待空闲线程。
  • 2、线程池中有可用线程时,从队列中取出这个任务执行,执行完毕后,线程归还到线程池,等待下个任务。同时以事件的方式通知event-loop,event-loop接收到事件执行该事件注册的回调函数。

当然,如果觉得4个线程不够用,可以在nodejs启动时,设置环境变量UV_THREADPOOL_SIZE来调整,出于系统性能考虑,libuv 规定可设置线程数不能超过128个。

nodejs源码

先简要介绍下nodejs的启动过程:

  • 1、调用platformInit方法 ,初始化 nodejs 的运行环境。
  • 2、调用 performance_node_start 方法,对 nodejs 进行性能统计。
  • 3、openssl设置的判断。
  • 4、调用v8_platform.Initialize,初始化 libuv 线程池。
  • 5、调用 V8::Initialize,初始化 V8 环境。
  • 6、创建一个nodejs运行实例。
  • 7、启动上一步创建好的实例。
  • 8、开始执行js文件,同步代码执行完毕后,进入事件循环。
  • 9、在没有任何可监听的事件时,销毁 nodejs 实例,程序执行完毕。

以上就是 nodejs 执行一个js文件的全过程。接下来着重介绍第八个步骤,事件循环。

我们看几处关键源码:

  • 1、core.c,事件循环运行的核心文件。
int uv_run(uv_loop_t* loop, uv_run_mode mode) {
  int timeout;
  int r;
  int ran_pending;
//判断事件循环是否存活。
  r = uv__loop_alive(loop);
  //如果没有存活,更新时间戳
  if (!r)
    uv__update_time(loop);
//如果事件循环存活,并且事件循环没有停止。
  while (r != 0 && loop->stop_flag == 0) {
    //更新当前时间戳
    uv__update_time(loop);
    //执行 timers 队列
    uv__run_timers(loop);
    //执行由于上个循环未执行完,并被延迟到这个循环的I/O 回调。
    ran_pending = uv__run_pending(loop); 
    //内部调用,用户不care,忽略
    uv__run_idle(loop); 
    //内部调用,用户不care,忽略
    uv__run_prepare(loop); 

    timeout = 0; 
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
    //计算距离下一个timer到来的时间差。
      timeout = uv_backend_timeout(loop);
   //进入 轮询 阶段,该阶段轮询I/O事件,有则执行,无则阻塞,直到超出timeout的时间。
    uv__io_poll(loop, timeout);
    //进入check阶段,主要执行 setImmediate 回调。
    uv__run_check(loop);
    //进行close阶段,主要执行 **关闭** 事件
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {

      //更新当前时间戳
      uv__update_time(loop);
      //再次执行timers回调。
      uv__run_timers(loop);
    }
    //判断当前事件循环是否存活。
    r = uv__loop_alive(loop); 
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  /* The if statement lets gcc compile it to a conditional store. Avoids   * dirtying a cache line.   */
  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}
  • 2、timers 阶段,源码文件:timers.c
void uv__run_timers(uv_loop_t* loop) {
  struct heap_node* heap_node;
  uv_timer_t* handle;

  for (;;) {
  //取出定时器堆中超时时间最近的定时器句柄
    heap_node = heap_min((struct heap*) &loop->timer_heap);
    if (heap_node == NULL)
      break;

    handle = container_of(heap_node, uv_timer_t, heap_node);
    // 判断最近的一个定时器句柄的超时时间是否大于当前时间,如果大于当前时间,说明还未超时,跳出循环。
    if (handle->timeout > loop->time)
      break;
    // 停止最近的定时器句柄
    uv_timer_stop(handle);
    // 判断定时器句柄类型是否是repeat类型,如果是,重新创建一个定时器句柄。
    uv_timer_again(handle);
    //执行定时器句柄绑定的回调函数
    handle->timer_cb(handle);
  }
}
  • 3、 轮询阶段 源码,源码文件:kquene.c
void uv__io_poll(uv_loop_t* loop, int timeout) {
  /*一连串的变量初始化*/
  //判断是否有事件发生    
  if (loop->nfds == 0) {
    //判断观察者队列是否为空,如果为空,则返回
    assert(QUEUE_EMPTY(&loop->watcher_queue));
    return;
  }

  nevents = 0;
  // 观察者队列不为空
  while (!QUEUE_EMPTY(&loop->watcher_queue)) {
    /*    取出队列头的观察者对象    取出观察者对象感兴趣的事件并监听。    */
    ....省略一些代码
    w->events = w->pevents;
  }


  assert(timeout >= -1);
  //如果有超时时间,将当前时间赋给base变量
  base = loop->time;
  // 本轮执行监听事件的最大数量
  count = 48; /* Benchmarks suggest this gives the best throughput. */
  //进入监听循环
  for (;; nevents = 0) {
  // 有超时时间的话,初始化spec
    if (timeout != -1) {
      spec.tv_sec = timeout / 1000;
      spec.tv_nsec = (timeout % 1000) * 1000000;
    }

    if (pset != NULL)
      pthread_sigmask(SIG_BLOCK, pset, NULL);
    // 监听内核事件,当有事件到来时,即返回事件的数量。
    // timeout 为监听的超时时间,超时时间一到即返回。
    // 我们知道,timeout是传进来得下一个timers到来的时间差,所以,在timeout时间内,event-loop会一直阻塞在此处,直到超时时间到来或者有内核事件触发。
    nfds = kevent(loop->backend_fd,
                  events,
                  nevents,
                  events,
                  ARRAY_SIZE(events),
                  timeout == -1 ? NULL : &spec);

    if (pset != NULL)
      pthread_sigmask(SIG_UNBLOCK, pset, NULL);

    /* Update loop->time unconditionally. It's tempting to skip the update when     * timeout == 0 (i.e. non-blocking poll) but there is no guarantee that the     * operating system didn't reschedule our process while in the syscall.     */
    SAVE_ERRNO(uv__update_time(loop));
    //如果内核没有监听到可用事件,且本次监听有超时时间,则返回。
    if (nfds == 0) {
      assert(timeout != -1);
      return;
    }

    if (nfds == -1) {
      if (errno != EINTR)
        abort();

      if (timeout == 0)
        return;

      if (timeout == -1)
        continue;

      /* Interrupted by a signal. Update timeout and poll again. */
      goto update_timeout;
    }

    。。。
    //判断事件循环的观察者队列是否为空
    assert(loop->watchers != NULL);
    loop->watchers[loop->nwatchers] = (void*) events;
    loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds;
    // 循环处理内核返回的事件,执行事件绑定的回调函数
    for (i = 0; i < nfds; i++) {
        。。。。
    }

}

参考 前端进阶面试题详细解答

uv__io_poll阶段源码最长,逻辑最为复杂,可以做个概括,如下:
当js层代码注册的事件回调都没有返回的时候,事件循环会阻塞在poll阶段。看到这里,你可能会想了,会永远阻塞在此处吗?

1、首先呢,在poll阶段执行的时候,会传入一个timeout超时时间,该超时时间就是poll阶段的最大阻塞时间。
2、其次呢,在poll阶段,timeout时间未到的时候,如果有事件返回,就执行该事件注册的回调函数。timeout超时时间到了,则退出poll阶段,执行下一个阶段。

所以,我们不用担心事件循环会永远阻塞在poll阶段。

以上就是事件循环的两个核心阶段。限于篇幅,timers阶段的其他源码和setImmediateprocess.nextTick的涉及到的源码就不罗列了,感兴趣的童鞋可以看下源码。

最后,总结出事件循环的原理如下,以上你可以不care,记住下面的总结就好了。

事件循环原理

  • node 的初始化
    • 初始化 node 环境。
    • 执行输入代码。
    • 执行 process.nextTick 回调。
    • 执行 microtasks。
  • 进入 event-loop
    • 进入 timers 阶段
      • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
      • 检查是否有 process.nextTick 任务,如果有,全部执行。
      • 检查是否有microtask,如果有,全部执行。
      • 退出该阶段。
    • 进入IO callbacks阶段。
      • 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
      • 检查是否有 process.nextTick 任务,如果有,全部执行。
      • 检查是否有microtask,如果有,全部执行。
      • 退出该阶段。
    • 进入 idle,prepare 阶段:
      • 这两个阶段与我们编程关系不大,暂且按下不表。
    • 进入 poll 阶段
      • 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
        • 第一种情况:
          • 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
          • 检查是否有 process.nextTick 回调,如果有,全部执行。
          • 检查是否有 microtaks,如果有,全部执行。
          • 退出该阶段。
        • 第二种情况:
          • 如果没有可用回调。
          • 检查是否有 immediate 回调,如果有,退出 poll 阶段。如果没有,阻塞在此阶段,等待新的事件通知。
      • 如果不存在尚未完成的回调,退出poll阶段。
    • 进入 check 阶段。
      • 如果有immediate回调,则执行所有immediate回调。
      • 检查是否有 process.nextTick 回调,如果有,全部执行。
      • 检查是否有 microtaks,如果有,全部执行。
      • 退出 check 阶段
    • 进入 closing 阶段。
      • 如果有immediate回调,则执行所有immediate回调。
      • 检查是否有 process.nextTick 回调,如果有,全部执行。
      • 检查是否有 microtaks,如果有,全部执行。
      • 退出 closing 阶段
    • 检查是否有活跃的 handles(定时器、IO等事件句柄)。
      • 如果有,继续下一轮循环。
      • 如果没有,结束事件循环,退出程序。

细心的童鞋可以发现,在事件循环的每一个子阶段退出之前都会按顺序执行如下过程:

  • 检查是否有 process.nextTick 回调,如果有,全部执行。
  • 检查是否有 microtaks,如果有,全部执行。
  • 退出当前阶段。

记住这个规律哦。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/268.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

14个Python处理Excel的常用操作,非常好用

自从学了Python后就逼迫用Python来处理Excel&#xff0c;所有操作用Python实现。目的是巩固Python&#xff0c;与增强数据处理能力。 这也是我写这篇文章的初衷。废话不说了&#xff0c;直接进入正题。 数据是网上找到的销售数据&#xff0c;长这样&#xff1a; 一、关联公式:…

人工智能轨道交通行业周刊-第35期(2023.2.20-2.26)

本期关键词&#xff1a;重庆智慧轨道、智能运维主机、标准轨距、地方铁路公报、景深、机器视觉应用 1 整理涉及公众号名单 1.1 行业类 RT轨道交通人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交通联盟VSTR铁路与城市轨道交通Rai…

【C/C++】必知必会知识点大总结

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;C/C知识点 &#x1f4e3;专栏定位&#xff1a;整理一下 C 相关的知识点&#xff0c;供大家学习参考~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;…

基于Reactor模式下的epoll多路复用服务器

文章目录一、认识Reactor模式1.1 Reactor 模式的概念1.2 Reactor 模式的组件1.3 Reactor 模式的流程1.4 Reactor 模式的优点二、Reactor模式下的 epoll ET服务器2.1 总体设计思路2.2 Connection 类结构2.3 封装 socket 实现 Sock 类2.4 封装 epoll 实现 Epoller 类2.4.1 Create…

2023年BeijngCrypt勒索病毒家族最新变种之.halo勒索病毒

目录 前言&#xff1a;简介 一、什么是.halo勒索病毒&#xff1f; 二、.halo勒索病毒是如何传播感染的&#xff1f; 三、感染.halo后缀勒索病毒建议立即做以下几件事情 四、中了.halo后缀的勒索病毒文件怎么恢复&#xff1f; 五、加密数据恢复情况 六、系统安全防护措施建…

宣布推出 .NET 社区工具包 8.1!

我们很高兴地宣布 .NET Community Toolkit 8.1 版正式发布&#xff01;这个新版本包括呼声很高的新功能、bug 修复和对 MVVM 工具包源代码生成器的大量性能改进&#xff0c;使开发人员在使用它们时的用户体验比以往更好&#xff01; 就像在我们之前的版本中一样&#xff0c;我…

STM32F1硬件SPI驱动nRF24L01通过按键控制数据收发带状态反馈

STM32F1硬件SPI驱动nRF24L01通过按键控制数据收发带状态反馈&#x1f4cc;相关篇《STM32F1基于STM32CubeMX配置硬件SPI驱动nRF24L01数据收发》 &#x1f3ac;功能演示 &#x1f33f;工程默认配置的是STM32F103VC单片机&#xff0c;其他型号的修改需要修改启动文件startup_st…

python+django+vue图书个性化推荐系统

整个系统是由多个功能模块组合而成的&#xff0c;要将所有的功能模块都一一列举出来&#xff0c;然后进行逐个的功能设计&#xff0c;使得每一个模块都有相对应的功能设计&#xff0c;然后进行系统整体的设计。 本图书个性化推荐系统结构图如图python manage.py runserver 开…

宇宙最强-GPT-4 横空出世:最先进、更安全、更有用

文章目录前言一、准确性提升1.创造力2.视觉输入3.更长的上下文二、相比于ChatGPT有哪些提升1.GPT-4 的高级推理能力超越了 ChatGPT2.GPT-4 在多种测试考试中均优于 ChatGPT。三、研究团队在GPT-4模型都做了哪些改善1.遵循 GPT、GPT-2 和 GPT-3 的研究路径2.我们花了 6 个月的时…

分享10个不错的C语言开源项目

今天跟大家分享10个重量级的C语言开源项目&#xff0c;C语言确实经得住考验&#xff1a; Redis&#xff1a;Redis是一个开源的高性能的键值对数据库。它以C语言编写&#xff0c;具有极高的性能和可靠性。 Nginx&#xff1a;Nginx是一个高性能的HTTP和反向代理服务器&#xff0…

刚工作3天就被裁了....

前言 还有谁&#xff1f;刚上三天班就被公司公司的工作不适合我&#xff0c;叫我先提升一下。 后面我也向公司那边讨要了一个说法&#xff0c;我只能说他们那边的说辞让我有些不服气。 现在之所以把这件事上记录一下&#xff0c;一是记录一下自己的成长轨迹&#xff0c;二是…

改进YOLO系列 | CVPR2023最新Backbone | FasterNet 远超 ShuffleNet、MobileNet、MobileViT 等模型

论文地址:https://export.arxiv.org/pdf/2303.03667v1.pdf 为了设计快速神经网络,许多工作都集中在减少浮点运算(FLOPs)的数量上。然而,作者观察到FLOPs的这种减少不一定会带来延迟的类似程度的减少。这主要源于每秒低浮点运算(FLOPS)效率低下。并且,如此低的FLOPS主要…

javaSE系列之类与对象

javaSE系列之类与方法什么是类类的定义书写事项什么是实例化this引用this的注意事项对象的初始化构造方法封装的概念访问限定符封装扩展之包static成员static的特性static的初始化代码块注意事项内部类1.实例内部类&#x1f497; &#x1f497; 博客:小怡同学&#x1f497; &am…

【LeetCode】1171. 从链表中删去总和值为零的连续节点、面试题 02.05. 链表求和

作者&#xff1a;小卢 专栏&#xff1a;《Leetcode》 喜欢的话&#xff1a;世间因为少年的挺身而出&#xff0c;而更加瑰丽。 ——《人民日报》 目录 1171. 从链表中删去总和值为零的连续节点 面试题 02.05. 链表求和 1171. 从链表中删去总和…

【面试题】面试官:如果后端给你 1w 条数据,你如何做展示?

最近一位朋友参加阿b的面试&#xff0c;然后面试官问了她这个问题&#xff0c;我问她咋写的&#xff0c;她一脸淡定的说&#xff1a;“虚拟列表。”大厂面试题分享 面试题库前后端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★地址&#xff1a;前端面…

tp6实现邮件发送

tp6实现邮件发送 phpMailer 是一个非常强大的 ph p发送邮件类,可以设定发送邮件地址、回复地址、邮件主题、html网页,上传附件,并且使用起来非常方便。 phpMailer 的特点&#xff1a; 1、在邮件中包含多个 TO、CC、BCC 和 REPLY-TO。2、平台应用广泛&#xff0c;支持的 SMTP…

阿里p8测试总监,让我们用这份《测试用例规范》,再也没加班过

经常看到无论是刚入职场的新人&#xff0c;还是工作了一段时间的老人&#xff0c;都会对编写测试用例感到困扰&#xff1f;例如&#xff1a; 固然&#xff0c;编写一份好的测试用例需要&#xff1a;充分的需求分析能力 理论及经验加持&#xff0c;作为测试职场摸爬打滚的老人&…

ElasticSearch 8 学习笔记总结(六)

文章目录一. ES8 的Java API 环境准备二. ES8 的Java API 索引操作三. ES8 的Java API 文档操作1. 文档的 插入 批量插入 删除等操作2. 文档的查询四、异步客户端操作一. ES8 的Java API 环境准备 ES8 废除了Type的概念。为了适应这种数据结构的改变&#xff0c;ES官方从1.7版…

学习 Python 之 Pygame 开发魂斗罗(十一)

学习 Python 之 Pygame 开发魂斗罗&#xff08;十一&#xff09;继续编写魂斗罗1. 改写主类函数中的代码顺序2. 修改玩家初始化3. 显示玩家生命值4. 设置玩家碰到敌人死亡5. 设置敌人子弹击中玩家6. 修改updatePlayerPosition()函数逻辑继续编写魂斗罗 在上次的博客学习 Pytho…

(四)HDFS双HA高可用机制

目录 概述 原理 主备切换 小结: 概述 进入到了hadoop 2.x的时代&#xff0c;为了保证namenode上的元数据不会丢失&#xff0c;而且是高可用的&#xff0c;出现了双实例HA的机制 原理 集群里启动两个namenode&#xff0c;一个是active状态(主)&#xff0c;一个是standby(备…