小心得浅析Nodejs Event Loop

Posted 360企业安全可视化实验室

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小心得浅析Nodejs Event Loop相关的知识,希望对你有一定的参考价值。


亲爱的小伙伴们

又到了分享小心得的时间

今天有请我们部门的姐妹花

——‘七只鱼’组合!

为大家带来Nodejs Event Loop方面的「小心得

【小心得】浅析Nodejs Event Loop



1

事件循环(Event Loop)

事件循环能让 Node.js 执行非阻塞 I/O 操作,尽管javascript事实上是单线程的,通过在可能的情况下把操作交给操作系统内核来实现。

由于大多数现代系统内核是多线程的,内核可以处理后台执行的多个操作。当其中一个操作完成的时候,内核告诉 Node.js,相应的回调就被添加到轮询队列(poll queue)并最终得到执行。


2

阶段总览

浏览器中与node中事件循环与执行机制不

在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:

【小心得】浅析Nodejs Event Loop


timers 阶段: 这个阶段执行setTimeout(callback) and setInterval(callback)预定的callback;
I/O callbacks 阶段:  是否有已完成的I/O操作的回调函数,来自上一轮的poll残留;
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(),process.nextTick不是基于libuv事件机制的,而timers一系列的api全部是基于libuv开放出来的api实现的。那么这个nextTick到底是如何实现的呢?我们先打个问号.


3

阶段细节

以下各阶段细节部分介绍来自原文:

The Node.js Event Loop


定时器(timers)

定时器的用途是让指定的回调函数在某个阈值后会被执行,具体的执行时间并不一定是那个精确的阈值。定时器的回调会在制定的时间过后尽快得到执行,然而,操作系统的计划或者其他回调的执行可能会延迟该回调的执行。


轮询(poll)

轮询阶段有两个主要功能:
1,执行已经到时的定时器脚本
2,处理轮询队列中的事件

当事件循环进入到轮询阶段却没有发现定时器时:
如果轮询队列非空,事件循环会迭代回调队列并同步执行回调,直到队列空了或者达到了上限(前文说过的根据操作系统的不同而设定的上限)。
如果轮询队列是空的:
如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调;
如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行。
一旦轮询队列空了,事件循环会查找已经到时的定时器。如果找到了,事件循环就回到定时器阶段去执行回调。


I/O callbacks

这个阶段执行一些诸如TCP错误之类的系统操作的回调。例如,如果一个TCP socket 在尝试连接时收到了 ECONNREFUSED错误,某些 *nix 系统会等着报告这个错误。这个就会被排到本阶段的队列中。


检查(check)

这个阶段允许回调函数在轮询阶段完成后立即执行。如果轮询阶段空闲了,并且有回调已经被 setImmediate() 加入队列,事件循环会进入检查阶段而不是在轮询阶段等待。
setImmediate() 是个特殊的定时器,在事件循环中一个单独的阶段运行。它使用libuv的API 来使得回调函数在轮询阶段完成后执行。


关闭事件的回调(close callbacks)

如果一个 socket 或句柄(handle)被突然关闭(is closed abruptly),例如 socket.destroy(), 'close' 事件会被发出到这个阶段。否则这种事件会通过 process.nextTick() 被发出。


setTimeout VS 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

为什么结果不确定呢?

解释:setTimeout/setInterval 的第二个参数取值范围是:[1, 2^31 - 1],如果超过这个范围则会初始化为 1,即 setTimeout(fn, 0) === setTimeout(fn, 1)。我们知道 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,event loop 的开始会先检查 timer 阶段,但是在开始之前到 timer 阶段会消耗一定时间,所以就会出现两种情况:

timer 前的准备时间超过 1ms,满足 loop->time >= 1,则执行 timer 阶段(setTimeout)的回调函数
timer 前的准备时间小于 1ms,则先执行 check 阶段(setImmediate)的回调函数,下一次 event loop 执行 timer 阶段(setTimeout)的回调函数
再看个例子:

setTimeout(() => {

  console.log('setTimeout')


}, 0)


setImmediate(() => {

  console.log('setImmediate')


})


const start = Date.now()


while (Date.now() - start < 10);


运行结果一定是:

setTimeout

setImmediate


4

process.nextTick实现

process.nextTick不是基于libuv事件机制的,而timers一系列的api全部是基于libuv开放出来的api实现的。那么这个nextTick到底是如何实现的呢?

接下来就要从nextTick的源码聊起了:

function _tickCallback() {

    let tock;

    do { while (tock = nextTickQueue.shift()) {


      const callback = tock.callback;


      if (tock.args === undefined)


          callback(); // ...


     }


      runMicrotasks();


    }


  // ...


  }

在执行完nextTick之后(callback())还继续执行了runMicrotasks,我相信如果了解过Microtasks的读者肯定知道这到底是做什么的,接下来我们深扒一下这个runMicrotasks:

// src/node.cc

v8::Local<v8::Function> run_microtasks_fn =


    env->NewFunctionTemplate(RunMicrotasks)->GetFunction(env->context())


        .ToLocalChecked();//v8 吐出来的方法 RunMicrotasks


run_microtasks_fn->SetName(


    FIXED_ONE_BYTE_STRING(env->isolate(), "runMicrotasks"));


    // deps/v8/src/isolate.cc


    void Isolate::RunMicrotasks() {// v8中RunMicrotasks实现


    // Increase call depth to prevent recursive callbacks.


    v8::Isolate::SuppressMicrotaskExecutionScope suppress(


    reinterpret_cast<v8::Isolate*>(this));


is_running_microtasks_ = true;


RunMicrotasksInternal();


is_running_microtasks_ = false;


FireMicrotasksCompletedCallback();


}


void Isolate::RunMicrotasksInternal() {

if (!pending_microtask_count())

    return;


TRACE_EVENT0("v8.execute", "RunMicrotasks");


TRACE_EVENT_CALL_STATS_SCOPED(this, "v8", "V8.RunMicrotasks");


while (pending_microtask_count() > 0) {


  HandleScope scope(this);


  int num_tasks = pending_microtask_count();


  Handle<FixedArray> queue(heap()->microtask_queue(), this);


  DCHECK(num_tasks <= queue->length());


  set_pending_microtask_count(0);


  heap()->set_microtask_queue(heap()->empty_fixed_array());// ...

通过上面的代码,可以比较清晰地看到整个RunMicrotasks的全过程,主要就是通过microtask_queue来实现的Microtask。
了解了整个流程,可以很容易得出一个结论:
nextTick会在v8执行Microtasks之前对在js中注册的nextTickQueue逐个执行,即阻塞了Microtasks执行。
后面简单的追踪了一下_tickCallback来证实一下最终_tickCallback传递给了tick_callback_function,追踪的过程小伙伴们有兴趣可以自行了解,最终可以总结为以下两点:
1.nextTick永远在主函数(包括同步代码和console)运行完之后运行
2.nextTick永远优先于microtask运行


5

浏览器Event Loop

浏览器中与node中事件循环与执行机制不同,不可混为一谈。 浏览器的Event loop是在html5中定义的规范,而node中则由libuv库实现。

macrotasks: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
microtasks: process.nextTick, Promises, Object.observe(废弃), MutationObserver


浏览器执行过程

执行完主执行线程中的任务。
取出Microtask Queue中任务执行直到清空。
取出Macrotask Queue中一个任务执行。
取出Microtask Queue中任务执行直到清空。
重复3和4。

即为同步完成,一个宏任务,所有微任务,一个宏任务,所有微任务......


来看个例子

console.log('script start');


setTimeout(function() {


console.log('setTimeout');


}, 0);


Promise.resolve().then(function() {


console.log('promise1');


}).then(function() {


console.log('promise2');


});

console.log('script end');

运行结果是:

script start

script endpromise1

promise2

setTimeout

解析:一开始task队列中只有script,则script中所有函数放入函数执行栈执行,代码按顺序执行。
接着遇到了setTimeout,它的作用是0ms后将回调函数放入task队列中,也就是说这个函数将在下一个事件循环中执行(注意这时候setTimeout执行完毕就返回了)。
接着遇到了Promise,按照前面所述Promise属于microtask,所以第一个.then()会放入microtask队列。
当所有script代码执行完毕后,此时函数执行栈为空。开始检查microtask队列,此时队列不为空,执行.then()的回调函数输出'promise1',由于.then()返回的依然是promise,所以第二个.then()会放入microtask队列继续执行,输出'promise2'。
此时microtask队列为空了,进入下一个事件循环,检查task队列发现了setTimeout的回调函数,立即执行回调函数输出'setTimeout',代码执行完毕。

以上便是浏览器事件循环的过程


更多文献可参考:
浏览器和Node不同的事件循环(https://link.jianshu.com/?t=https%3A%2F%2Fjuejin.im%2Fpost%2F5aa5dcabf265da239c7afe1e%3Futm_medium%3Dfe%26utm_source%3Dweixinqun)


6

总结

以下各阶段细节部分介绍来自原文:



【小心得】浅析Nodejs Event Loop

绿色小块是 macrotask(宏任务),macrotask 中间的粉红箭头是 microtask(微任务)。

1.nodejs event loop分了六个不同的阶段执行,而process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。
2.nodejs event loop 执行过程
清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
进入下轮循环。
3.process.nextTick为代表的 microtask执行仍然将 tick 函数注册到当前 microtask 的尾部,而setTimeout等宏任务会注册到下一次事件循环中。
4.浏览器中与node中事件循环与执行机制不同,浏览器的eventloop是在HTML5中定义的规范,而node中则由libuv实现。


【小心得】浅析Nodejs Event Loop




好啦,今天的分享就到这里啦~

小伙伴们有任何小心得

也欢迎留言与我们讨论互动喔!


◆  ◆  

我们会不断地分享更多有趣的干货~

笔芯【小心得】浅析Nodejs Event Loop~




以上是关于小心得浅析Nodejs Event Loop的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript Event Loop 机制详解与 Vue.js 中实践应用

Nodejs深度探秘:event loop的本质和异步代码中的Zalgo问题

Nodejs深度探秘:event loop的本质和异步代码中的Zalgo问题

不要在nodejs中阻塞event loop

不要在nodejs中阻塞event loop

Nodejs Event Loop