Nodejs 解读event loop的事件处理机制

摘要:

1. nodejs 为什么要存在一个event loop的事件处理机制?
2. event loop的事件处理机制如何运作的?
3. 从event loop机制的角度上区分setImmediate()与setTimeout()
4. 从event loop机制的角度上区分process.nextTick()与setImmediate()

noted:内容来源对nodejs 官方文档学习总结

1. nodejs 为什么要存在一个event loop的事件处理机制?

nodejs 具有事件驱动和非阻塞但线程的特点,使相关应用变得比较轻量和高效。当应用程序需要相关I/O操作时,线程并不会阻塞,而是把I/O操作移交给底层类库(如:libuv)。此时nodejs线程会去处理其他的任务,当底层库处理完相关的I/O操作后,会将主动权再次交还给nodejs线程。因此event loop的作用就是起到调度线程的作用,如当底层类库处理I/O操作后调度nodejs单线程处理后续的工作。也就是说当nodejs 程序启动的时候,它会开启一个event loop以实现异步的api调度、schedule timers 、回调process.nextTick()。

从上也可以看出nodejs 虽说是单线程,但是在底层类库处理异步操作的时候仍然是多线程。

2. event loop的事件处理机制如果运作的?

eventloop.jpg

上述的五个阶段都是按照先进先出的规则执行回调函数。按顺序执行每个阶段的回调函数队列,直至队列为空或是该阶段执行的回调函数达到该阶段所允许一次执行回调函数的最大限制后,才会将操作权移交给下一阶段。

每个阶段的简单概要:

  • timers: 执行setTimeout() 和 setInterval() 预先设定的回调函数。
  • I/O callbacks: 大部分执行都是timers 阶段或是setImmediate() 预先设定的并且出现异常的回调函数事件。
  • idle, prepare: nodejs 内部函数调用。
  • poll: 搜寻I/O事件,nodejs进程在这个阶段会选择在该阶段适当的阻塞一段时间。
  • check: setImmediate() 函数会在这个阶段执行。
  • close callbacks: 执行一些诸如关闭事件的回调函数,如socket.on('close', ...) 。

每个阶段的详细内容:

  • poll
    该阶段主要是两个任务:
    1. 当timers 的定时器到时后,执行定时器(setTimeout 和 setInternal)的回调函数。
    2. 执行poll 队列里面的I/O 队列。
poll phrase.png

Noted:在poll phrase一旦event loop中的poll queue队列为空,poll 就会去timers 查看有没有到期的定时期需要执行。如果有,就会返回timer执行相应的回调函数。

  • timers
    指定线程执行定时器(setTimeout和 setInterval)的回调函数,但是大多数的时候定时器的回调函数执行的时间要远大于定时器设定的时间。因为必须要等poll phrase中的poll queue队列为空时,poll才会去查看timer中有没有到期的定时器然后去执行定时器中的回调函数。
const fs = require('fs');

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/Users/spursy/Develop/TFDemo/activateTF.txt', callback);
}

const timeoutScheduled = Date.now();

setTimeout(function() {

  const delay = Date.now() - timeoutScheduled;

  console.log(delay + 'ms have passed since I was scheduled');
}, 100);


// do someAsyncOperation which takes 94 ms to complete
someAsyncOperation(function() {

  const startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    // do nothing
  }

});

上面的函数执行的结果是:

104ms have passed since I was scheduled

定时器加入到timer中,定时的时间设置为100秒。poll中执行的I/O操作,由于读取相应目录下的文件要耗费一些时间,poll将会阻塞在这里循环相应的回调函数,大约在94秒时相应的I/O操作执行完毕,对应的回调函数又耗费了10秒钟。这时poll queue为空,此时poll会去timer查看有没有到期的定时器。发现存在一个已经超时近4秒的定时器然后就执行定时器对应的回调函数,这样就是定时器执行了将近104秒钟时间的原因。

  • I/O callbacks
    该阶段执行一些诸如TCP的errors回调函数。
  • check
    如果poll中已没有排队的队列,并且存在setImmediate() 立即执行的回调函数,这是event loop不会在poll阶段阻塞等待相应的I/O事件,而是直接去check阶段执行setImmediate() 函数。
  • close callback
    该阶段执行close的事件函数。

3. 从event loop机制的角度上区分setImmediate()与setTimeout()

从Issue 2中poll和check阶段的逻辑,我们可以看出setImmediate和setTimeout、setInterval都是在poll 阶段执行完当前的I/O队列中相应的回调函数后触发的。但是这两个函数却是由不同的路径触发的。

setImmediate函数,是在当前的poll queue对列执行后为空或是执行的数目达到上限后,event loop直接调入check阶段执行setImmediate函数。
setTimeout、setInterval则是在当前的poll queue对列执行后为空或是执行的数目达到上限后,event loop去timers检查是否存在已经到期的定时器,如果存在直接执行相应的回调函数。

如果程序中既有setTimeout和setImmediate,两者的执行顺序是什么?

// timeout_vs_immediate.js
setTimeout(function timeout() {
  console.log('timeout');
}, 0);

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

上面的程序执行的结果并不是唯一的,有时immediate在前,有时timeout在qian。主要是由于他们运行的当前上下文环境中存在其他的程序影响了他们执行顺序。

// timeout_vs_immediate.js
const fs = require('fs');

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

上面的程序把setImmediate和setTimeout放到了I/O循环中,此时他们的执行顺序永远都是immediate在前,timeout在后。

4. 从event loop机制的角度上区分process.nextTick()与setImmediate()

1. process.nextTick()函数

  • 尽管process.nextTick()也是一个异步的函数,但是它并没有出现在上面event loop的结构图中。不管当前正在event loop的哪个阶段,在当前阶段执行完毕后,跳入下个阶段前的瞬间执行process.nextTick()函数。
  • 由于process.nextTick()函数的特性,很可能出现一种恶劣的情形:在event loop进入poll前调用该函数,就会阻止程序进入poll阶段allows you to "starve" your I/O by making recursive process.nextTick() calls
  • 但是也正是nodejs的一个设计哲学:每个函数都可以是异步的,即使它不必这样做。例如下面程序片段,如果不对内部函数作异步处理就可能出现异常。
let bar;

// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }

// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {

  // since someAsyncApiCall has completed, bar hasn't been assigned any value
  console.log('bar', bar); // undefined

});

bar = 1;

由于someAsyncApiCall函数在执行时,内部函数是同步的,这是变量bar还没有被赋值。如果改为下面的就会这个异常。

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;

2. 两者的比较

  • process.nextTick() 函数是在任何阶段执行结束的时刻
  • setImmediate() 函数是在poll阶段后进去check阶段事执行

3. process.nextTick() 函数的应用

  • 允许线程在进入event loop下一个阶段前做一些关于处理异常、清理一些无用或无关的资源。l例如下面:
function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback,
                            new TypeError('argument should be string'));
}
  • 在进入下个event loop阶段前,并且回调函数还没有释放回调权限时执行一些相关操作。如下代码:
const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function() {
    this.emit('event');
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
  console.log('an event occurred!');
});

在MyEmitter构造函数实例化前注册“event”事件,这样就可以保证实例化后的函数可以监听“event”事件。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容