当前位置: 首页 > 后端技术 > Node.js

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

时间:2023-04-03 19:59:18 Node.js

前沿Node.js是一个基于V8引擎的javascript运行环境。Node.js具有事件驱动、非阻塞I/O等特点,结合NodeAPI,Node.js具有网络编程、文件系统等服务端功能,Node.js使用libuv库进行异步事件处理。线程Node.js单线程的意思其实就是指执行同步代码的主线程。一个Node程序的启动不仅分配了一个线程,而且我们只能在一个线程中执行代码。当有I/O资源调用、TCP连接等外部资源申请时,主线程不会阻塞,而是委托给I/O线程处理,进入Waiting队列。一旦主线程执行完毕,就会消耗事件队列(EventQueue)。因为主线程只有一个,只占用CPU核心处理逻辑计算,不适合CPU密集型使用。注意上图中的EVENT_QUEUE,给人的感觉好像只有一个队列。根据Node.js官方介绍,EventLoop有6个stage,每个stage都有对应的先进先出回调队列。什么是事件循环(EventLoop)?在计算机科学中,事件循环、消息调度器、消息循环、消息泵或运行循环是一种编程结构,它在程序中等待和调度事件或消息。--来自wiki关于意思:EventLoop是一种常用的机制,通过内部或外部的事件提供者发出请求,如文件读写、网络连接等异步操作,完成后调用事件处理器。整个过程就是异步阶段Node.js事件循环机制当Node.js启动时,会初始化事件循环,处理提供的输入脚本(或者落入REPL,本文档不涉及),可能会做异步API电话、日程计时器,或调用process.nextTick(),然后开始处理事件循环。--来自node.jsdoc大致意思:Node.js启动时,会初始化一个事件循环,处理脚本时可能会出现异步API行为调用,使用定时器任务或nexTick,处理完成后,进入事件循环处理流程事件循环阶段┌──────────────────────────────────────────────────────┐┌──>│定时器……──┐││I/O回调││└──────────────┬─────────────────────────────────────────────────────────────────────────────────────────────┘┌────────────────────────────────────────────────────————————┴────────────────┐││空闲,准备││└────────────┬────────────────────┘┌──────────────────┐│┌────────────┴───────────────┐┐传入:│││投票│<───────┤连接,││└──────────────┬───────────────┘│数据等││┌──────────────────────────────────────────┴────────────────────────────┘│检查│└────────────────────────────┘│┌────────────┴──────────────┐└──┤收盘回调│└────────────────────────────┘每个阶段都有一个FIFO回调队列,每个阶段都有自己的事件处理方法。当事件循环进入到某个阶段,就会在这个阶段执行回调,直到队列耗尽或者执行完最大回调次数,才会进入下一个处理阶段。timers阶段:这个阶段执行setTimeout(callback)和setInterval(callback)计划的回调;I/O回调阶段:执行关闭事件回调、定时器设置的回调(定时器、setTimeout、setInterval等)、setImmediate()回调以外的回调;(currentstage)idle,preparestage:仅供节点内部使用;poll阶段:获取新的I/O事件,节点在合适的情况下会阻塞在这里;检查阶段:执行setImmediate()设置的回调;closecallbacks阶段:比如socket.on('close',callback)的回调会在这个阶段执行。以下是creeperyang的节选。对于以上6个阶段(原文翻译)定时器阶段,一个定时器指定了一个下限时间,而不是确切的时间。达到这个最短时间后执行回调在指定的时间过去后,定时器会尽早执行回调,但系统调度或其他回调执行可能会延迟它们。注意:从技术上讲,轮询阶段控制计时器何时执行。注意:这个下限时间有一个范围:[1,2147483647],如果设置的时间不在这个范围内,则设置为1。I/O回调阶段这个阶段对一些系统操作进行回调。比如TCP错误,比如一个TCPsocket在尝试连接的时候收到ECONNREFUSED,类unix系统会等待报错,然后放入I/O回调阶段的队列中执行。该名称可能会误导为执行I/O回调处理程序,实际上I/O回调将由轮询阶段处理。轮询阶段轮询阶段有两个主要功能:执行达到最小时间的定时器的回调,然后处理轮询队列中的事件。当事件循环进入轮询阶段,并且没有设置定时器(没有调度定时器)时,会发生以下两种情况之一:如果轮询队列不为空,事件循环将遍历队列并执行同步回调,直到队列为空或执行的回调次数达到系统上限;如果轮询队列为空,将发生以下两种情况之一:如果代码已通过setImmediate()回调设置,事件循环将结束轮询阶段并进入检查阶段执行检查队列(在回调中).如果代码没有通过setImmediate()设置,事件循环会阻塞在这个阶段,等待回调被加入到轮询队列中,并立即执行。但是,当事件循环进入轮询阶段并设置了定时器后,一旦轮询队列为空(轮询阶段空闲状态):事件循环将检查定时器,如果一个或多个定时器的下限时间已到到达后,事件循环将绕回定时器阶段,并执行定时器队列。检查阶段此阶段允许在轮询阶段结束后立即执行回调。如果轮询阶段空闲并且有setImmediate()设置的回调,事件循环将进入检查阶段而不是等待。setImmediate()实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。它使用libuv的API将回调设置为在轮询阶段结束后立即执行。一般来说,随着代码的执行,事件循环最终会进入轮询阶段,在这个阶段等待传入的连接、请求等。但是只要有setImmediate()设置的回调,一旦poll阶段空闲,程序就会结束poll阶段进入check阶段,而不是继续等待poll事件(pollevents)。closecallbacks阶段如果一个socket或handle突然关闭(比如socket.destroy()),close事件会在这个阶段被触发,否则会被process.nextTick()触发SimpleEventLoopconstfs=require('fs');letcounts=0;functionwait(mstime){letdate=Date.now();while(Date.now()-date{console.log('timers',Date.now()-lastTime+'ms');},0);process.nextTick(()=>{//执行wait(20)之前进入事件循环//定时器阶段;asyncOperation(()=>{console.log('poll');});});/***result:*timers21ms*poll*/为了让setTimeout优先在fs.readFile回调之上,执行了process.nextTick,也就是说在进入timers阶段之前,等待20ms后才执行filereading.nextTick和setImmediateprocess.nextTick不属于eventloop的任何阶段,属于transition本阶段和下一阶段之间,即本阶段执行结束前、进入下一阶段前要执行的回调。给人一种插队的感觉。setImmediate的回调处于检查阶段。当poll阶段的队列为空且check阶段的event队列存在时,会切换到check阶段执行。nextTick递归的危害因为nextTick有一个跳队机制,nextTick的递归会使事件循环机制无法进入下一阶段。导致I/O处理完成或超时后定时任务仍无法执行,导致其他事件处理程序处于饥饿状态。为了防止递归带来的问题,Node.js提供了一个process.maxTickDepth(默认1000)。递归nextTickconstfs=require('fs');让计数=0;functionwait(mstime){让date=Date.now();while(Date.now()-date{wait(20);nextTick();});}constlastTime=Date.now();setTimeout(()=>{console.log('timers',Date.now()-lastTime+'ms');},0);nextTick();这个时候是绝对不能跳转到timer阶段的,因为在进入timers阶段之前还有连续的nextTick插入执行。除非执行1000次达到执行上限。如果setImmediate在I/O周期内调度,setImmediate()将始终在任何计时器之前执行。setTimeout和setImmediatesetImmediate()设计用于在轮询阶段结束后立即执行回调;setTimeout()设计在达到指定的下限时间后执行回调;不带I/O处理输出未定义!setTimeout(fn,0)有几毫秒的不确定性。不能保证进入定时器阶段,定时器就能立即执行处理程序。在I/O事件处理程序下varfs=require('fs')fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout')},0)setImmediate(()=>{console.log('immediate')})})此时setImmediate先于setTimeout执行,因为poll阶段执行完成后进入check阶段。计时器阶段在下一个事件循环阶段。相关文章分析Node.js单线程模型Node.jsEventLoopUnderstandingTimers,process.nextTick()Node.jseventloopandtimer/setImmediate/nextTickTheNode.jsEventLoop,Timers,andprocess.nextTick()