资讯专栏INFORMATION COLUMN

【Node.js】理解事件循环机制

Riddler / 2482人阅读

摘要:前沿是基于引擎的运行环境具有事件驱动非阻塞等特点结合具有网络编程文件系统等服务端的功能用库进行异步事件处理线程的单线程含义实际上说的是执行同步代码的主线程一个程序的启动不止是分配了一个线程,而是我们只能在一个线程执行代码当出现资源调用连接等

前沿

Node.js 是基于V8引擎的javascript运行环境. Node.js具有事件驱动, 非阻塞I/O等特点. 结合Node API, Node.js 具有网络编程, 文件系统等服务端的功能, Node.js用libuv库进行异步事件处理.

线程

Node.js的单线程含义, 实际上说的是执行同步代码的主线程. 一个Node程序的启动, 不止是分配了一个线程,而是我们只能在一个线程执行代码. 当出现I/O资源调用, TCP连接等外部资源申请的时候, 不会阻塞主线程, 而是委托给I/O线程进行处理,并且进入等待队列. 一旦主线程执行完成,将会消费事件队列(Event Queue). 因为只有一个主线程, 只占用CPU内核处理逻辑计算, 因此不适合在CPU密集型进行使用.

注意,上图的EVENT_QUEUE 给人看起来是只有一个队列, 根据Node.js官方介绍, EventLoop有6个阶段, 同时每个阶段都有对应的一个先进先出的回调队列.

什么是事件循环(EventLoop) ?

In computer science, the event loop, message dispatcher, message loop, message pump, or run loop is a programming construct that waits for and dispatches events or messages in a program. -- from wiki

大概含义: EventLoop 是一种常用的机制,通过对内部或外部的事件提供者发出请求, 如文件读写, 网络连接 等异步操作, 完成后调用事件处理程序. 整个过程都是异步阶段

Node.js的事件循环机制

When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop. -- from node.js doc

大致含义: 当Node.js 启动, 就会初始化一个 event loop, 处理脚本时, 可能会发生异步API行为调用, 使用定时器任务或者nexTick, 处理完成后进入事件循环处理过程

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

每一个阶段都有一个FIFO的callbacks队列, 每个阶段都有自己的事件处理方式. 当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段.

timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;

I/O callbacks 阶段: 执行除了close事件的callbacks、被timers(定时器,setTimeout、setInterval等)设定的callbacks、setImmediate()设定的callbacks之外的callbacks; (目前这个阶段)

idle, prepare 阶段: 仅node内部使用;

poll 阶段: 获取新的I/O事件, 适当的条件下node将阻塞在这里;

check 阶段: 执行setImmediate() 设定的callbacks;

close callbacks 阶段: 比如socket.on(‘close’, callback)的callback会在这个阶段执行.

下面是摘抄creeperyang 对上面6个阶段的 (原文翻译)

timers阶段

一个timer指定一个下限时间而不是准确时间,在达到这个下限时间后执行回调。在指定时间过后,timers会尽可能早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。

注意:技术上来说,poll 阶段控制 timers 什么时候执行。

注意:这个下限时间有个范围:[1, 2147483647],如果设定的时间不在这个范围,将被设置为1。

I/O callbacks阶段

这个阶段执行一些系统操作的回调。比如TCP错误,如一个TCP socket在想要连接时收到ECONNREFUSED,
类unix系统会等待以报告错误,这就会放到 I/O callbacks 阶段的队列执行.
名字会让人误解为执行I/O回调处理程序, 实际上I/O回调会由poll阶段处理.

poll阶段

poll 阶段有两个主要功能:

执行下限时间已经达到的timers的回调,然后
处理 poll 队列里的事件。
当event loop进入 poll 阶段,并且 没有设定的timers(there are no timers scheduled),会发生下面两件事之一:

如果 poll 队列不空,event loop会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;

如果 poll 队列为空,则发生以下两件事之一:

如果代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里的回调)。

如果代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。

但是,当event loop进入 poll 阶段,并且 有设定的timers,一旦 poll 队列为空(poll 阶段空闲状态):

event loop将检查timers,如果有1个或多个timers的下限时间已经到达,event loop将绕回 timers 阶段,并执行 timer 队列。

check阶段

这个阶段允许在 poll 阶段结束后立即执行回调。如果 poll 阶段空闲,并且有被setImmediate()设定的回调,event loop会转到 check 阶段而不是继续等待。

setImmediate()实际上是一个特殊的timer,跑在event loop中一个独立的阶段。它使用libuv的API
来设定在 poll 阶段结束后立即执行回调。

通常上来讲,随着代码执行,event loop终将进入 poll 阶段,在这个阶段等待 incoming connection, request 等等。但是,只要有被setImmediate()设定了回调,一旦 poll 阶段空闲,那么程序将结束 poll 阶段并进入 check 阶段,而不是继续等待 poll 事件们 (poll events)。

close callbacks 阶段

如果一个 socket 或 handle 被突然关掉(比如 socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发

简单的 EventLoop

const fs = require("fs");
let counts = 0;

function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

function asyncOperation (callback) {
  fs.readFile(__dirname + "/" + __filename, callback);
}

const lastTime = Date.now();

setTimeout(() => {
  console.log("timers", Date.now() - lastTime + "ms");
}, 0);

process.nextTick(() => {
  // 进入event loop
  // timers阶段之前执行
  wait(20);
  asyncOperation(() => {
    console.log("poll");
  });  
});

/**
 * result:
 * timers 21ms
 * poll
 */

为了让setTimeout优先于fs.readFile 回调, 执行了process.nextTick, 表示在进入 timers阶段前, 等待20ms后执行文件读取.

nextTick 与 setImmediate

process.nextTick 不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调。有给人一种插队的感觉.

setImmediate的回调处于check阶段, 当poll阶段的队列为空, 且check阶段的事件队列存在的时候,切换到check阶段执行.

nextTick 递归的危害
由于nextTick具有插队的机制,nextTick的递归会让事件循环机制无法进入下一个阶段. 导致I/O处理完成或者定时任务超时后仍然无法执行, 导致了其它事件处理程序处于饥饿状态. 为了防止递归产生的问题, Node.js 提供了一个 process.maxTickDepth (默认 1000)。

递归nextTick

const fs = require("fs");
let counts = 0;

function wait (mstime) {
  let date = Date.now();
  while (Date.now() - date < mstime) {
    // do nothing
  }
}

function nextTick () {
  process.nextTick(() => {
    wait(20);
    nextTick();
  });
}

const lastTime = Date.now();

setTimeout(() => {
  console.log("timers", Date.now() - lastTime + "ms");
}, 0);

nextTick();

此时永远无法跳到timer阶段, 因为在进入timers阶段前有不断的nextTick插入执行. 除非执行了1000次到了执行上限.

setImmediate
如果在一个I/O周期内进行调度,setImmediate()将始终在任何定时器之前执行.

setTimeout 与 setImmediate

setImmediate()被设计在 poll 阶段结束后立即执行回调;

setTimeout()被设计在指定下限时间到达后执行回调;

无 I/O 处理情况下

setTimeout(function timeout () {
  console.log("timeout");
},0);

setImmediate(function immediate () {
  console.log("immediate");
});

输出结果是 不确定 的!
setTimeout(fn, 0) 具有几毫秒的不确定性. 无法保证进入timers阶段, 定时器能够立即执行处理程序.

在I/O事件处理程序下

var fs = require("fs")

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log("timeout")
  }, 0)
  setImmediate(() => {
    console.log("immediate")
  })
})

此时 setImmediate 优先于 setTimeout 执行,因为 poll阶段执行完成后 进入 check阶段. timers阶段处于下一个事件循环阶段了.

相关文章

浅析 Node.js 单线程模型

Node.js Event Loop 的理解 Timers,process.nextTick()

Node.js的event loop及timer/setImmediate/nextTick

The Node.js Event Loop, Timers, and process.nextTick()

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90145.html

相关文章

  • 真正理解 Node.js事件循环

    摘要:轮询投票处理下一次处理的新事件立即设置运行通过注册的所有回调关闭执行所有的回调工作处理延迟此度量标准测量线程池处理异步任务需要多长时间。高工作时间处理延迟表明繁忙耗尽的线程池。 原文=> What you should know to really understand the Node.js Event Loop Node.js 是一个基于事件的平台。这就意味着在Node中发生的所...

    GeekGhc 评论0 收藏0
  • node核心特性理解

    摘要:概述本文主要介绍了我对的一些核心特性的理解,包括架构特点机制核心模块与简单应用。在此期间,主线程继续执行其他任务。延续了浏览器端单线程,只用一个主线程执行,不断循环遍历事件队列,执行事件。 原文地址在我的博客,转载请注明来源,谢谢! node是在前端领域经常看到的词。node对于前端的重要性已经不言而喻,掌握node也是作为合格的前端工程师一项基本功了。知道node、知道后端的一些东西...

    huangjinnan 评论0 收藏0
  • 初窥JavaScript事件机制的实现(一)—— Node.js事件驱动实现概览

    摘要:如果当前没有事件也没有定时器事件,则返回。相关资料关于的架构及设计思路的事件讨论了使用线程池异步运行代码。下一篇初窥事件机制的实现二中定时器的实现 在浏览器中,事件作为一个极为重要的机制,给予JavaScript响应用户操作与DOM变化的能力;在Node.js中,事件驱动模型则是其高并发能力的基础。 学习JavaScript也需要了解它的运行平台,为了更好的理解JavaScript的事...

    lavor 评论0 收藏0
  • 用一道大厂面试题带你搞懂事件循环机制

    本文涵盖 面试题的引入 对事件循环面试题执行顺序的一些疑问 通过面试题对微任务、事件循环、定时器等对深入理解 结论总结 面试题 面试题如下,大家可以先试着写一下输出结果,然后再看我下面的详细讲解,看看会不会有什么出入,如果把整个顺序弄清楚 Node.js 的执行顺序应该就没问题了。 async function async1(){ console.log(async1 start) ...

    ShowerSun 评论0 收藏0
  • 【新手向】Node.js事件循环中的:Macrotask 与 Microtask

    摘要:一句话解释在事件循环机制中,有任务两个队列队列和队列。设置任务为目前运行的任务,并执行。应该是考虑到了这一点,至少任务中的任务,是被设置了在一个事件循环中的最大调用次数的,叫。参考材料理解事件循环 在Node学习过程中,不可避免的需要对事件循环机制做深入理解,其中Macrotask(大型任务)和Microtask(小型任务)比较令人困惑,在一番google之后,我发现了几篇资料能比较好...

    CoderDock 评论0 收藏0

发表评论

0条评论

Riddler

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<