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

前沿

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

线程

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

Node核心

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

什么是事件循环(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 wikinode

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

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 docgithub

大体含义: 当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会尽量早地执行回调,但系统调度或者其它回调的执行可能会延迟它们。socket

注意:技术上来讲,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 队列为空,则发生如下两件事之一:

  1. 若是代码已经被setImmediate()设定了回调, event loop将结束 poll 阶段进入 check 阶段来执行 check 队列(里的回调)。
  2. 若是代码没有被setImmediate()设定回调,event loop将阻塞在该阶段等待回调被加入 poll 队列,并当即执行。

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

  1. 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阶段处于下一个事件循环阶段了.

相关文章

相关文章
相关标签/搜索