Node.js Event Loop 的理解 Timers,process.nextTick()
发布于 2 年前 作者 vincentLiuxiang 11476 次浏览 最后一次编辑是 1 年前 来自 分享

写node.js有一段时间了,一直在理解event loop这个概念,国内外的文章翻阅了也不少,但是对event loop能讲解清楚的还是不多。 最后还是查阅的nodejs对event loop的官方解释才恍然大悟,如获至宝。

写这篇文章的目的是将自己对该文章的理解做一个记录,官方文档链接The Node.js Event Loop, Timers, and process.nextTick()

文章内容可能有错误理解的地方,希望能和大家探讨一下,欢迎批评指正!

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

Event Loop的解释

英文原文: 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.

当Node.js启动时会初始化event loop, 每一个event loop都会包含按如下顺序六个循环阶段,

   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close 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会在这个阶段执行.

每一个阶段都有一个装有callbacks的fifo queue(队列),当event loop运行到一个指定阶段时, node将执行该阶段的fifo queue(队列),当队列callback执行完或者执行callbacks数量超过该阶段的上限时, event loop会转入下一下阶段.

注意上面六个阶段都不包括 process.nextTick()

poll阶段

poll阶段是衔接整个event loop各个阶段比较重要的阶段,为了便于后续例子的理解,本文和原文的介绍顺序不一样,本文先讲这个阶段;

在node.js里,任何异步方法(除timer,close,setImmediate之外)完成时,都会将其callback加到poll queue里,并立即执行。

poll 阶段有两个主要的功能:

  1. 处理poll队列(poll quenue)的事件(callback);
  2. 执行timers的callback,当到达timers指定的时间时;

如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:

  • 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
  • 如果poll queue为空,将会发生下面情况:
    • 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
    • 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue;

如果event loop进入了 poll阶段,且代码设定了timer:

  • 如果poll queue进入空状态时(即poll 阶段为空闲状态),event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.

以上便是整个event loop时间循环的各个阶段运行机制,有了这层理解,我们来看几个例子

  • 注意,例子中给出的时间在不同机器下和同一机器下不同执行时刻,其值都会有差异。

example 1

var fs = require('fs');

function someAsyncOperation (callback) {
  // 花费2毫秒
  fs.readFile(__dirname + '/' + __filename, callback);
}

var timeoutScheduled = Date.now();
var fileReadTime = 0;

setTimeout(function () {
  var delay = Date.now() - timeoutScheduled;
  console.log('setTimeout: ' + (delay) + "ms have passed since I was scheduled");
  console.log('fileReaderTime',fileReadtime - timeoutScheduled);
}, 10);

someAsyncOperation(function () {
  fileReadtime = Date.now();
  while(Date.now() - fileReadtime < 20) {

  }
});

结果: 先执行someAsyncOperation的callback,再执行setTimeout callback

-> node eventloop.js
setTimeout: 22ms have passed since I was scheduled
fileReaderTime 2

解释: 当时程序启动时,event loop初始化:

  1. timer阶段(无callback到达,setTimeout需要10毫秒)
  2. i/o callback阶段,无异步i/o完成
  3. 忽略
  4. poll阶段,阻塞在这里,当运行2ms时,fs.readFile完成,将其callback加入 poll队列,并执行callback, 其中callback要消耗20毫秒,等callback之行完,poll处于空闲状态,由于之前设定了timer,因此检查timers,发现timer设定时间是20ms,当前时间运行超过了该值,因此,立即循环回到timer阶段执行其callback,因此,虽然setTimeout的20毫秒,但实际是22毫秒后执行。

example 2

var fs = require('fs');

function someAsyncOperation (callback) {
  var time = Date.now();
  // 花费9毫秒
  fs.readFile('/path/to/xxxx.pdf', callback);
}

var timeoutScheduled = Date.now();
var fileReadTime = 0;
var delay = 0;

setTimeout(function () {
  delay = Date.now() - timeoutScheduled;
}, 5);

someAsyncOperation(function () {
  fileReadtime = Date.now();
  while(Date.now() - fileReadtime < 20) {

  }
  console.log('setTimeout: ' + (delay) + "ms have passed since I was scheduled");
  console.log('fileReaderTime',fileReadtime - timeoutScheduled);
});

结果:setTimeout callback先执行,someAsyncOperation callback后执行

-> node eventloop.js
setTimeout: 7ms have passed since I was scheduled
fileReaderTime 9

解释: 当时程序启动时,event loop初始化:

  1. timer阶段(无callback到达,setTimeout需要10毫秒)
  2. i/o callback阶段,无异步i/o完成
  3. 忽略
  4. poll阶段,阻塞在这里,当运行5ms时,poll依然空闲,但已设定timer,且时间已到达,因此,event loop需要循环到timer阶段,执行setTimeout callback,由于从poll --> timer中间要经历check,close阶段,这些阶段也会消耗一定时间,因此执行setTimeout callback实际是7毫秒 然后又回到poll阶段等待异步i/o完成,在9毫秒时fs.readFile完成,其callback加入poll queue并执行。

setTimeout 和 setImmediate

二者非常相似,但是二者区别取决于他们什么时候被调用.

  • setImmediate 设计在poll阶段完成时执行,即check阶段;
  • setTimeout 设计在poll阶段为空闲时,且设定时间到达后执行;但其在timer阶段执行

其二者的调用顺序取决于当前event loop的上下文,如果他们在异步i/o callback之外调用,其执行先后顺序是不确定的

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

setImmediate(function immediate () {
  console.log('immediate');
});
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

关于这点的原因,笔者目前还未弄清楚。。。其原因读者可去看@hyj1991 的留言,讲解非常清晰

但当二者在异步i/o callback内部调用时,总是先执行setImmediate,再执行setTimeout

var fs = require('fs')

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})
$ node timeout_vs_immediate.js
immediate
timeout

理解了event loop的各阶段顺序这个例子很好理解: 因为fs.readFile callback执行完后,程序设定了timer 和 setImmediate,因此poll阶段不会被阻塞进而进入check阶段先执行setImmediate,后进入timer阶段执行setTimeout

process.nextTick()

千呼万唤始出来,终于到了讲process.nextTick()的时候,来来来喝口水休息休息。

注意:本文讲解的process.nextTick是基于v0.10及以上版本

process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。

var fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(()=>{
      console.log('nextTick3');
    })
  });
  process.nextTick(()=>{
    console.log('nextTick1');
  })
  process.nextTick(()=>{
    console.log('nextTick2');
  })
});
-> node eventloop.js
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout

从poll —> check阶段,先执行process.nextTick, nextTick1 nextTick2 然后进入check,setImmediate, setImmediate 执行完setImmediate后,出check,进入close callback前,执行process.nextTick nextTick3 最后进入timer执行setTimeout setTimeout

process.nextTick()是node早期版本无setImmediate时的产物,node作者推荐我们尽量使用setImmediate。

example process.nextTick()

当递归调用process.nextTick时,即使fs.readFile完成,其callback无机会执行

var fs = require('fs');
var starttime = Date.now();
var endtime = 0;

fs.readFile(__filename, () => {
  endtime = Date.now();
  console.log('finish reading time:',endtime - starttime);
});

var index = 0;

function nextTick () {
  if (index > 1000) return;
  index++;
  console.log('nextTick');
  process.nextTick(nextTick);
}

nextTick();
-> node eventloop.js
nextTick
nextTick
...
nextTick
nextTick
finish reading time: 246

example setImmediate

将process.nextTick替换成setImmediate后,由于setImmediate只在check阶段执行,那么所有的callback都有机会执行。

var fs = require('fs');

fs.readFile(__filename, () => {
  console.log('finish reading');
});

var index = 0;

function Immediate () {
  if (index > 100) return;
  index++;
  console.log('setImmediate');
  setImmediate(Immediate);
}

Immediate()
-> node eventloop.js
setImmediate
setImmediate
setImmediate
setImmediate
finish reading time: 19
...
setImmediate
setImmediate
43 回复

感谢分享

@kevin1211 希望对你有用:)

其实event是由uv_run驱动的,并且是在UV_RUN_ONCE的模式下执行 UV_RUN_ONCE就是你文中描述的

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

中的“适当条件”,它会阻塞在kqueue的kevent()函数中(这里以mac为例,linux下这一块由epoll_wait()函数处理,windows下则是IOCP) 这里描述UV_RUN_ONCE为适当条件,是因为uv_run还有两种模式,对于UV_RUN_NOWAIT的模式,io_poll不会阻塞,会立即触发超时结束当前的event_loop,进入下一次循环(v4.4.2版本中仅用在进程退出前的最后检查) 实际上,你描述的timer,在一次event loop中可能在两个地方执行:

最开始以及close callbacks结束之后

为什么这么说呢,就算不看源代码,我们也可以思考这样的一个场景: 当你设置了一个超时timer,那么第一次进入uv_run_timer时,超时时间未到;此时进入kevent()阻塞等待 如果此处不提供超时机制,那就会永远阻塞在kevent()的I/O等待中;所以我们在 当前的event loop循环中的某一次时,如果存在timer且为达到设定时间,则会把最近的一个timer剩余超时时间作为参数传入io_poll()中,这样kevent()等待时,如果没有任何I/O事件触发,也会由timerout触发跳出等待的操作,结束本次时间循环 所以呢,在UV_RUN_ONCE的模式下,每次循环结束前,即你说的close callback执行结束后,会再执行一次对timer的超时判断 具体涉及到的代码如下:

//deps/uv/src/unix/core.c
int uv_run(uv_loop_t *loop, uv_run_mode mode) {
	int timeout;
	int r;
	int ran_pending;
	//uv__loop_alive返回的是event loop中是否还有待处理的handle或者request
	//以及closing_handles是否为NULL,如果均没有,则返回0
	r = uv__loop_alive(loop);
	//更新当前event loop的时间戳,单位是ms
	if (!r)
    	uv__update_time(loop);
	while (r != 0 && loop->stop_flag == 0) {
    	//使用Linux下的高精度Timer hrtime更新loop->time,即event loop的时间戳
    	uv__update_time(loop);
    	//执行判断当前loop->time下有无到期的Timer,显然在同一个loop里面timer拥有最高的优先级
    	uv__run_timers(loop);
    	//判断当前的pending_queue是否有事件待处理,并且一次将&loop->pending_queue中的uv__io_t对应的cb全部拿出来执行
    	ran_pending = uv__run_pending(loop);
    	//实现在loop-watcher.c文件中,一次将&loop->idle_handles中的idle_cd全部执行完毕(如果存在的话)
    	uv__run_idle(loop);
    	//实现在loop-watcher.c文件中,一次将&loop->prepare_handles中的prepare_cb全部执行完毕(如果存在的话)
    	uv__run_prepare(loop);

    	timeout = 0;
    	//如果是UV_RUN_ONCE的模式,并且pending_queue队列为空,或者采用UV_RUN_DEFAULT(在一个loop中处理所有事件),则将timeout参数置为
    	//最近的一个定时器的超时时间,防止在uv_io_poll中阻塞住无法进入超时的timer中
    	if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
        	timeout = uv_backend_timeout(loop);
    	//进入I/O处理的函数(重点分析的部分),此处挂载timeout是为了防止在uv_io_poll中陷入阻塞无法执行timers;并且对于mode为
    	//UV_RUN_NOWAIT类型的uv_run执行,timeout为0可以保证其立即跳出uv__io_poll,达到了非阻塞调用的效果
    	uv__io_poll(loop, timeout);
    	//实现在loop-watcher.c文件中,一次将&loop->check_handles中的check_cb全部执行完毕(如果存在的话)
    	uv__run_check(loop);
    	//执行结束时的资源释放,loop->closing_handles指针指向NULL
    	uv__run_closing_handles(loop);

    	if (mode == UV_RUN_ONCE) {
        	//如果是UV_RUN_ONCE模式,继续更新当前event loop的时间戳
        	uv__update_time(loop);
        	//执行timers,判断是否有已经到期的timer
        	uv__run_timers(loop);
    	}
    	r = uv__loop_alive(loop);
    	//在UV_RUN_ONCE和UV_RUN_NOWAIT模式中,跳出当前的循环
    	if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
        	break;
		}
		
	//标记当前的stop_flag为0,表示当前的loop执行完毕
	if (loop->stop_flag != 0)
    	loop->stop_flag = 0;
	//返回r的值
	return r;
}

对于node事件循环的核心驱动函数uv_run写的一些注释,随便看看吧~

@hyj1991 谢谢你的注释,我只是看了官方文档,加了自己的一些理解,

我会结合你的注释去看看源码。 请教个问题

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

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

执行的顺序不确定,就是因为每一次loop,最开始和结束时都检查timer的缘故么?

@vincentLiuxiang 是的,在node中,setTimeout(cb, 0) === setTimeout(cb, 1); 而setImmediately属于uv_run_check的部分 确实每次loop进来,都是先检查uv_run_timer的,但是由于cpu工作耗费时间,比如第一次获取的hrtime为0 那么setTimeout(cb, 1),超时时间就是loop->time = 1(ms,node定时器精确到1ms,但是hrtime是精确到纳秒级别的) 所以第一次loop进来的时候就有两种情况:

1.由于第一次loop前的准备耗时超过1ms,当前的loop->time >=1 ,则uv_run_timer生效,timeout先执行
2.由于第一次loop前的准备耗时小于1ms,当前的loop->time = 0,则本次loop中的第一次uv_run_timer不生效,那么io_poll后先执行uv_run_check,即immediate先执行,然后等close cb执行完后,继续执行uv_run_timer

那么你说的为什么在回调中,一定是先immediate执行呢,其实也很容易理解 你可以思考一下你写的场景 由于你的timeout和immediate的事件注册是在readFile的回调执行时,触发的 所以必然的,在readFile的回调执行前的每一次event loop进来的uv_run_timer都不会有超时事件触发 那么当readFile执行完毕,kevent收到监听的fd事件完成后,执行了该回调,此时

1.timeout事件注册
2.immediate事件注册
3.由于readFile的回调执行完毕,那么就会从uv_io_poll中出来,此时立即执行uv_run_check,所以immediate事件被执行掉
4.最后的uv_run_timer检查timeout事件,执行timeout事件

所以你会发现,在I/O回调中注册的两者,永远都是immediately先执行

@hyj1991 当时看文档的时候,这块儿就没太弄明白,太感谢了!写的非常清楚。

@vincentLiuxiang 不客气哈,不过我也蛮佩服你的,看文档就能理清这么多哇。我当时是查文档看的晕,资料也少,无奈下直接去读源代码的。。。

感谢分享,精彩的讨论。

楼主真是真知灼见啊

mark

来自酷炫的 CNodeMD

先收藏一波~~~

@JacksonTian 大佬觉得这篇文章说的对不对

文档链接貌似404了。专门去旧分支找链接 https://github.com/nodejs/node/blob/v6.x/doc/topics/event-loop-timers-and-nexttick.md 搜到了这里,拜读了。

@shellphon 老早前写的文章了,谢啦~ 我去重新编辑一下

补充点: 我的疑问:poll 中执行了 i/o callback 为什么还要I/O callbacks。。 “node原理介绍 ” 引用:查阅了libuv 的文档后发现,在 libuv 的 event loop 中,I/O callbacks 阶段会执行 Pending callbacks。绝大多数情况下,在 poll 阶段,所有的 I/O 回调都已经被执行。但是,在某些情况下,有一些回调会被延迟到下一次循环执行。也就是说,在 I/O callbacks 阶段执行的回调函数,是上一次事件循环中被延迟执行的回调函数。 不知道 对不对0 0

@LastKing 你可以参考下源码

 while (r != 0 && loop->stop_flag == 0) {
    uv__update_time(loop);
    uv__run_timers(loop);
	// 下面这一段就是i/o callbacks阶段,来专门执行pending_queue
    ran_pending = uv__run_pending(loop);
    uv__run_idle(loop);
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);

    uv__io_poll(loop, timeout);
    uv__run_check(loop);
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      /* UV_RUN_ONCE implies forward progress: at least one callback must have
       * been invoked when it returns. uv__io_poll() can return without doing
       * I/O (meaning: no callbacks) when its timeout expires - which means we
       * have pending timers that satisfy the forward progress constraint.
       *
       * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from
       * the check.
       */
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

额,其实很久以前有个老外讲得还蛮清楚的: http://latentflip.com/loupe/ ,虽然不是专门给 node.js 写的。

@LastKing

严格来说,i/o callbacks并不是处理文件i/o的callback 而是处理一些系统调用错误,比如网络 stream, pipe, tcp, udp通信的错误callback。参考 因为,pending_queue的入列(queue_insert_tail)是通过一个叫 uv__io_feed 的api来调用的 而 uv__io_feed API是在tcp/udp/stream/pipe等相关API调用

@vincentLiuxiang 谢谢 !~ 源码阅读能力不够。多谢 指导!~

var fs = require('fs');
var events = require('events');
var emitter = new events.EventEmitter();

emitter.on('someEvent', function(arg1, arg2) {
	console.log('listener');
});

function someAsyncOperation (callback) {
  // 花费2毫秒
  fs.readFile(__dirname + '/' + __filename, callback);
}

var timeoutScheduled = Date.now();
var fileReadTime = 0;

setTimeout(function () {
  var delay = Date.now() - timeoutScheduled;
  console.log('setTimeout: ' + (delay) + "ms have passed since I was scheduled");
  console.log('fileReaderTime',fileReadtime - timeoutScheduled);
}, 10);

someAsyncOperation(function () {
  fileReadtime = Date.now();
  while(Date.now() - fileReadtime < 20) {
	
  }
  
  setImmediate(function(){
	  console.log('setImmediate');
  })
  emitter.emit('someEvent');
});

运行结果:

listener
setImmediate
setTimeout: 31ms have passed since I was scheduled
fileReaderTime 2

@vincentLiuxiang,请问emitter.emit(‘someEvent’);这个的事件处理是在poll阶段吗? 在处理’someEvent’事件的时候是不是都一直在poll阶段,没有阶段切换? 因为如果有阶段切换,那么结果一定是setImmediate先于listener。 不知这样分析是否正确。

@qiulanzhu 不是!

events,timer 不是libuv层面实现的,应该是v8层实现。

所谓 check / poll 阶段是针对libuv层面实现的。

虽然 timer (settimeout) 不是libuv层面实现,但其在uv_run 内执行,因此才有timer阶段。

events注册的事件, 其实是存在了一个eventqueue里,

// 以下是伪代码
e.on('test', cb);
// 等价于 eventqueue['test'].push(cb)
e.emit('test');
// 等价于 eventqueue['test'].map(cb => cb()),这是伪代码

因此,可以看出,event是同步的!当执行 emit触发时,会先于上述六个阶段执行。

具体例子,和源码分析,等过段时间再贴。。。最尽有点忙

例子没有关于io callback阶段的?为什么readfile 的callback在poll阶段完成?

@isbdnt 等有空了写一个吧。。。

在19楼回复了原理,至于例子,你也可以去写写

也就是说网络io cb在io cb阶段完成吧,并不想自己写,我是伸手党,嘻嘻

@vincentLiuxiang 非常感谢,这样讲清晰多了!

@hyj1991 @vincentLiuxiang
看了好多网上的资料和书,都没用搞懂node是怎样使用epoll完成网络I/O的,大多数资料只重点讲了使用线程池实现文件I/O,但对于网络I/O都是直接说用epoll一带而过。我个人觉得可能是我对epoll不够理解造成的。我不明白的有以下几点: 1.event loop 是不是跑在node主线程上的?执行完同步代码后,再去一遍遍的循环,还是说event loop实际上另外一条线程。 2.网络请求来时候,node会不会再新起一个线程去处理。或者说,用哪个线程去处理,是主线程自行处理吗? 3.epoll会阻塞线程,若使用epoll 的是主线程,那主线程岂不是被epoll阻塞了?如果一直没有网络连接,会一直阻塞到这吗?

@Julyrainy

  • 1)event loop 是跑在node主线程上的,while true一直执行uv_run,
  • 2)会的,请求来的时候,已经丢在event loop 里的任务了
  • 3)epoll操作都在thread pool里,只有机器没有执行资源,才会阻塞。

@Julyrainy epoll主要有三个api

epoll_create
epoll_ctl
epoll_wait

要理解 node 的 epoll 模型,epoll 的原理还是先要去理解的。 这里我简单讲一下:

    1. epollfd = epoll_create(size), 创建 epoll 句柄;
    1. epoll_ctl(epollfd, Mode, other_fd, &event), 设置epollfd,监听的事件类型
    1. event_number = epoll_wait(epollfd, events, max_size, timeout),
    • epoll等待事件到来,event_number表示到来的事件数量,events通常是空数组,直到事件来临后,events会从0开始,把event_number个事件装到events里,timeout表示阻塞时间,-1将永久等待,直到有监听事件到来;

基于这个基础,那么在node里是怎么做的呢? 我贴一段 libuv的核心代码片段( linux-core.c ),你看看就懂了

...
// uv__io_poll, poll阶段

nfds,是ready事件的数量
...
nfds = uv__epoll_wait(loop->backend_fd,
                             events,
                             ARRAY_SIZE(events),
                             timeout,
                             sigmask);
...
// epoll cb执行

for (i = 0; i < nfds; i++) {
      pe = events + i;
      fd = pe->data;
 	  w = loop->watchers[fd];
     ....
     // 执行 回调 函数,epoll 网络相关部分,我读代码是没发现有 所谓 thread pool,就是单线程。
     w->cb(loop, w, pe->events);

     nevents++;
}

epoll_wait 阻塞返回后,回调函数就在uv_run主线程里执行,是没有多线程的. 因此,很容易理解一个现象,当我们在回调函数里执行 while(1) {}, 整个服务都无法响应了。

至于是epoll 好 还是 多线程/线程池好,这个google一下nginx相关文章有很多分析的。 还有我所说的线程是 用户态的应用层 线程, 不是硬件/物理线程。物理线程就那么多,应用层线程池大了,来回切换也是开销。

@hyj1991 @i5ting 也请大神们看看,我理解不一定对。

@vincentLiuxiang 对 epoll 和 uv_run 描述的比较清晰,没问题的。不过 uv_run 实现了跨平台,在 mac 下使用的不是 epoll 而是 kqueue,但是大致的原理是一致的

@hyj1991 嗯啊,我主要分析 linux-core.c , 其他平台我浅以为是类似的,而 epoll 被大家说得最多。 而且还有人把它和多线程混在一起谈。epoll 在 linux 老版本里多线程有惊群现象,在内核 3.x 还是 4.x 版本(具体忘了)有个内核设置可以解决该问题。

@i5ting 多谢狼叔指点!

@vincentLiuxiang 多谢您的耐心指点,我大概已经有些了解了。也就是说epoll在等待网络I/O数据就绪的时候,一直是阻塞的,此时无法响应如文件I/O和计时器事件,直到所有网络I/O完成或者超时,才能继续event loop是吗?

@Julyrainy 下一份 node 源码,自己编译/运行printf一下,什么都清晰了。

@vincentLiuxiang ok,主要是我基础比较差,想先了解一下原理再去看源码,看来我还是得死死地啃一遍源码,多谢您耐心的回答。

c的高手 搞js 很简单呀

请问, node事件循环中是否有体现浏览器 宏任务(macrotask) 和 微任务(microtask) 机制的地方? 如果有, 它与上述文中所说的node事件循环机制有何联系? 如何把他们串起来、讲得通? 浏览器中的macrotask、microtask 知乎macrotask、microtask 谢谢!

@MrStronger 突然发现有5个月没上 cnode 了,两者没关系。。。

@vincentLiuxiang @hyj1991 大佬好 请问 如果process.nextTick是在事件循环的每个phase结束后执行,那么这样的一段代码:

setTimeout(function() {
	console.log('timeout');
}, 0);
setImmediate(function() {
	console.log('setImmediate');
});
process.nextTick(function() {
	console.log('nextTick');
});

为什么process.nextTick总是第一个执行? 已知在第一次进入event loop的时候,有可能准备时间大于1ms,于是uv_run_timer调用回调函数,那不是应该先执行setTimeout,然后在切换到下一个phase的中间来执行process.nextTick吗? 还是说在每次进入event loop的时候先会去执行一次process.nextTick,然后再开始update_time,进入timer阶段?

@ProfutW @ProfutW 进入 event loop 前会清空一次 next tick queue 的

来自酷炫的 CNodeMD

@ProfutW 具体实现没记错的话在 lib/module.js 的 runMain 方法里面

来自酷炫的 CNodeMD

@hyj1991 好的 谢谢大佬了 我去看看

回到顶部