• Node.js 的事件循环


      当启动node程序时,比如 node index.js, index.js 就会从上到下依次执行 ,执行完毕后,就会进入到事件循环阶段。事件循环从事件队列中取出事件(回调函数),发送给JS引擎去执行。很简单,是吧! 但是Node.js的事件循环并不是循环一个队列 ,而是有多个队列,不同类型的事件放到不同的队列中,而且,这些队列,还来自不同的地方,libuv中提供了队列,Node.js本身也提供的队列。这就很容易产生一个问题,Node.js是怎么轮询这些队列的?首先Node.js为libuv中的队列分了阶段,每一个阶段都包含一个队列(先进先出的队列),事件循环到了某一个阶段就会取出队列中的事件(回调函数)发送给JS引擎去执行。其次,每执行完一个阶段,它会查看Node.js本身提供的队列有没有事件,有就执行,没有就进行到下一个阶段,先看libuv中的阶段

       timer阶段:包含过期的setTimeout, setInterval的回调函数组成的队列;当用setTimeout或setInterval 添加一个定时器时,Node.js会把定时器和相应的回调函数放到定时器堆(一种数据结构)中。每一次事件循环执行到timer阶段,都会调用系统时间,到堆中看看有没有定时器过期,有多少个定时器过期,如果有,就会把对应的回调函数,放到队列中。那Node.js是怎么查看过期的?

    const myTimer = setTimeout(function a(){console.log('Timer executed')},15);
    console.log(myTimer);

      setTimeout 返回一个timeOut对象,它有两个属性,一个是_idleTimeout, 就是设计的15ms, 一个是_idleStart:它是Node程序启动后,执行setTimeout语句时创建的时间。只要执行到timer阶段,减去这两个时间,就会计算出有没有过期。

      pending callback 阶段:包含系统相关的回调函数组成的队列。比如写了node服务,监听8080端口,但8080端口被占用了,Node抛出了错误,如果你监听错误,注册了回调函数,回调函数就会执行。但一些Linux操作系统,想让这个回调函数等一下再执行,因为它要处理一些其它的事情,像这样的回调函数就会放到pending callback队列中。

      idle和prepare阶段,是内部使用的队列。

      poll阶段:包含I/O相关的回调函数组成的队列;

      check阶段:包含setImmediate注册的回调函数。

      close callbacks 阶段,包含close事件的回调函数组成的队列,比如socket.on('close', callback); callback就会放到这个队列中。

      事件循环就是按照 timer -> pending callback -> idle和prepare -> poll -> check -> close callbacks 的顺序循环,每到一个阶段就会把队列中的回调函数发送JS引擎去执行。当然我们真正关心的是timer阶段,poll阶段,check阶段和close callbacks 阶段。

      事件循环每执行完一个阶段,不是都会检查Node.js 提供的队列吗? 是的, Node.js本身提供了两个队列, process.nexttick 回调函数队列,和microtasks队列,比如promise 回调函数组成的队列。不过要注意,process.nextick队列的优先级比microtaks队列的高,也就是先检查process.nextick队列,再检查microtask队列,只有next tick队列中的所有事件都执行比毕,才会执行microtask 队列中的事件。Node.js 整个事件循环如下图所示

      图中有几个点,前面没有提到,Node.js 的事件循环是不是可以不开启?是的,如果没有事件队列中规定的事件发生,它就不会开启,比如程序是 const sum = 1; 它也就没有必要开启事件循环了。Node.js中的事件循环是不是可以退出?是的,如果事件队列中的事件都执行完了,没有事情可以做了,它也就退出了。timer阶段和Check阶段的事件执行过程也有所改变,每执行完队列中的一个事件,都会检查nextTick队列和microtask队列,而不是把该阶段队列中的所有事件都执行完,再检查nextTick队列和microtask队列。

    setTimeout(() => {
        console.log('setTimeout 1');
    }, 0)
    
    setTimeout(() => {
        console.log('setTimeout 2');
        Promise.resolve().then(() => {
            console.log('setTimeout2 Promise');
        })
    }, 0)
    
    setTimeout(() => {
        console.log('setTimeout 3');
    }, 0)

      上面代码的执行结果是

    setTimeout 1
    setTimeout 2       
    setTimeout2 Promise
    setTimeout 3

      setTimeout2执行完以后,它就会检查micorTask队列,有一个事件,所以就执行了,再回到timer队列继续执行,setTimeout3执行了。poll阶段有一个等待动作,如果事件循环执行到poll阶段,队列中有I/O事件,它会把队列中的所有I/O事件都执行完毕,然后再计算要不要在这个地方等待其它I/O的完成,如果没有事件,它就会直接计算要不要等待。I/O 都处理完了,也没pending的I/O请求,它就不用等了。再者,如果close 阶段有事件,它就会执行close 事件,也不用这里等。 如果两者都不是,比如有pending 的I/O 或close没有事件,它要不要在这等,还要取决于时间,如果setTime, setImmediat, 设置了handle,如果过期了,它也不会停止在polling, 它会执行timer事件,如果它们没有过期,node会计算,到过期时间的间隔,然后等待这个间隔,如果没有setimeout,setimmediate 等, 它会一直在这里等。setimmediate 是一个特殊的时间。polling也就解释了服务器程序一直不停止的原因。

       nextTick和microtask队列除了在事件循环的每个阶段执行外,主程序执行完,也会先检查这两个队列,

    Promise.resolve().then(() => console.log('promise1 resolved'));
    Promise.resolve().then(() => {
        console.log('promise2 resolved');
        process.nextTick(() => console.log('next tick inside promise resolve handler'));
    });
    
    process.nextTick(() => console.log('next tick1'));
    
    setImmediate(() => console.log('set immedaite1'));
    
    setTimeout(() => console.log('set timeout'), 0);

      程序输出结果

    next tick1
    promise1 resolved
    promise2 resolved
    next tick inside promise resolve handler
    set timeout
    set immedaite1

      主程序执行完,得知此时有待处理的 next tick 回调,Node 将会执行它们直到队列为空。然后检查 promises 微任务队列,队列中有回调需要被执行,开始执行这些回调,在处理 promises 微任务队列的过程中,有一个 next tick 回调被添加到 nexttick 队列中。在 promises 微任务队列完成之后,得知 next tick 队列中有一个回调通过 promises 微任务添加到了队列中,然后 node 会再一次执行 next tick 队列中的那一个回调任务。执行完 promises 和 next tick 的所有任务之后,事件循环会移动到第一个阶段即定时器阶段,此时它将会发现在定时器队列中有一个到期的定时器回调需要被执行,然后执行该回调。执行完定时器队列中所有回调之后,事件循环到了poll阶段,队列中没有事件,并且setImmediate设置了回调,事件循环则会移动到 check阶段。它将会检测到有待执行处理的回调,事件循环会将它们逐一执行。最后,事件循环完成了所有事件... 然后程序退出。

       完整的Node.js的事件循环或Node.js的执行过程

      在Poll阶段,EventLoop在等待I/O的完成?Node.js是怎么做异步I/O的?I/O event是谁放到队列中的?Node.js有一个Event demultiplexer(事件多路分发机制)的概念 Event Demultiplexer接受到I/O请求后,就会交给相应的硬件去处理。一旦这个I/O请求被处理完成,就会把这个I/O 对应的事件处理函数,放到队列中。

      但是现实并不是这么简单,Event Demultiplexer并不是一个真实存在的组件,它只是一个抽象的概念,在不同的操作系统中有不同的实现名称,比如在Linux,它叫epoll,在Mac上,它叫kqueue, 在windows上,它叫IOCP( IOCP (Input Output Completion Port))。Node就是消费这些实现提供的异步,非block的硬件I/O功能。但并不是所有类型的I/O都能用硬件的异步I/O功能。网络I/O可以使用epoll, Kqueue,IOCP来实现,但文件I/O不行,比较复杂,比如Linux并不支持完全异步的文件读取。在Mac上,文件系统的异步通知也有一定的限制,为了提供完整的异步来解决所有这些文件系统的复杂性,几乎不可能,因此Node.js的提供了线程池,来支持这些I/O。只要I/O不能通过硬件的异步I/O功能(epoll/kqueue/IOCP)来解决,就使用线程池.  因此并不是所有的I/O功能都发生在线程池中,Node.js会尽最大可能地使用非阻塞的,异步硬件I/O, 对于那些阻塞的或非常复杂,才能解决的I/O类型,它使用线程池。高CPU消耗的功能也是使用线程池,比如压缩,加密,避免阻塞事件循环。

       总之,在现实的世界中,在不同类型的操作系统,支持所有的不同类型的I/O是非常困难的,一些I/O是使用原生支持的异步,一些则使用线程池来保证异步。为了把这些复杂细节封装起来,libuv出现,它暴露了一层API给Node上层。Event Demultiplexer就可以看做是Libuv抽象出来的,供Node.js上层调用的处理I/O的API 的集合。

       事件循环的几个细节

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

      以上程序的输出结果并不能被保证。node.js内部,最小的timeout是1ms,也就是即使写了0,node.js也会把它变成1ms。当每一次开始eventloop,node.js都会调用系统时间,来看看timer是不是过期。根据当时的cpu情况,获到系统时间,可能需要小于1ms的时间,也可能需要大于1ms的时间。如果时间少于1ms,那么Node.js就会觉得timer 并不过期,回调函数就不会被执行,EventLoop 就会到下一个阶段,I/O,再到immediate,如果获取时间大于1ms,过期了,它就会执行setTimeout的回调函数。可以通过下面的程序来看一下,Node在看有没有过期的时候,每次都会调用系统时间

    const start = process.hrtime();
    
    setTimeout(() => {
        const end = process.hrtime(start);
        console.log(`timeout callback executed after ${end[0]}s and ${end[1]/Math.pow(10,9)}ms`);
    }, 1000);

      这个回调函数都是1s多一点才执行,执行回调函数之前,它都要调用系统时间来检查有没有过期

  • 相关阅读:
    elasticsearch 调整参数 调参 -- 副本 分片 读写流程 -- elasticsearch配置
    对创新的理解 -- 价值共生、协同生长
    update_by_query ingest pipeline
    create index pattern Forbidden error
    apache转发规则 + nginx location 正则匹配经典案例
    拨测ip+port 告警 telnet nc
    configMap简单理解
    sidecar收集Tomcat日志-普通用户
    使用DBeaver连接pheonix
    bladex开发自己的服务不推送服务器的方法
  • 原文地址:https://www.cnblogs.com/SamWeb/p/16113219.html
Copyright © 2020-2023  润新知