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

吃透nodejs事件循环_0

时间:2023-04-03 14:03:16 Node.js

nodejs是单线程执行的,也是事件驱动的非阻塞IO编程模型。这使我们可以继续执行代码,而无需等待异步操作的结果返回。当触发异步事件时,通知主线程,主线程执行相应事件的回调。以上是众所周知的内容。今天我们从源码入手,分析一下nodejs的事件循环机制。nodejs架构首先我们来看下nodejs架构,如下图所示:如上图所示,nodejs从上到下分为用户代码(js代码)。用户代码就是我们写的应用代码,npm包,以及nodejs内置的js模块等等,我们日常工作中大部分时间都是在写这个层面的代码。绑定代码或者第三方插件(js或者C/C++代码)胶水代码可以让js调用C/C++代码。可以理解为一座桥,js在桥的一端,C/C++在桥的另一端。通过这个桥梁,js可以调用C/C++。在nodejs中,胶水代码的主要作用是将nodejs底层实现的C/C++库暴露给js环境。三方插件是自己实现的C/C++库,需要自己实现胶水代码,桥接js和C/C++。底层库nodejs依赖库,包括大名鼎鼎的V8和libuv。V8:我们都知道它是google开发的一个高效的javascript运行时,而nodejs之所以能高效的执行js代码,主要就在它里面。libuv:是一套用C语言实现的异步函数库。nodejs高效的异步编程模型很大程度上要归功于libuv的实现,而libuv正是我们今天要重点介绍的。还有一些其他的依赖库http-parser:负责解析http响应openssl:加解密c-ares:dns解析npm:nodejs包管理器...nodejs就不多介绍了,大家可以查看学习靠自己,接下来我们分析的重点是libuv。libuv架构我们知道nodejs的异步机制的核心是libuv,它负责nodejs与文件、网络等异步任务之间的沟通桥梁。下图给我们对libuv的一个大概印象:这是libuv官网的一部分如图所示,很明显nodejs的网络I/O,文件I/O,DNS操作,一些用户代码都是在libuv工作。说完异步,我们先总结一下nodejs中的异步事件:非I/O:timer(setTimeout,setInterval)microtask(promise)process.nextTicksetImmediateDNS.lookupI/O:网络I/O文件I/O一些DNS操作...网络I/O对于网络I/O,各个平台的实现机制不同。Linux是epoll模型,unix是kquene,windows是高效的IOCP完成端口,SunOs是事件端口,而libuv支持对这些网络I/O模型进行封装。参考nodejs进阶视频讲解:进入学习文件I/O,异步DNS操作libuv也维护了一个默认的4个线程的线程池,这些线程负责执行文件I/O操作,DNS操作,用户异步代码。当js层传递一个操作任务给libuv时,libuv会将该任务加入到队列中。之后有两种情况:1.当线程池中的所有线程都被占用时,队列中的任务会排队等待空闲线程。2、当线程池中有可用线程时,从队列中取出任务执行。执行完成后,线程返回线程池,等待下一个任务。同时以事件的形式通知event-loop,event-loop接收事件并执行事件注册的回调函数。当然,如果觉得4个线程不够用,可以在nodejs启动时设置环境变量UV_THREADPOOL_SIZE来调整。出于系统性能的考虑,libuv规定可以设置的线程数不能超过128个。nodejs源码首先简单介绍下nodejs的启动过程:1.调用platformInit方法初始化nodejs运行环境。2、调用performance_node_start方法对nodejs进行性能统计。3、openssl设置的判断。4.调用v8_platform.Initialize初始化libuv线程池。5.调用V8::Initialize初始化V8环境。6.创建一个nodejs运行实例。7.启动上一步创建的实例。8.开始执行js文件,执行完同步代码后进入事件循环。9.当没有可以监听到的事件时,销毁nodejs实例,程序执行完毕。以上就是nodejs执行一个js文件的全过程。接下来,我们将重点介绍第八步,事件循环。我们来看几个关键源码:1.core.c,事件循环运行的核心文件。intuv_run(uv_loop_t*loop,uv_run_modemode){inttimeout;诠释;intran_pending;//判断事件循环是否存活。r=uv__loop_alive(loop);//如果不存活,更新时间戳if(!r)uv__update_time(loop);//如果事件循环存活,并且事件循环没有停止。while(r!=0&&loop->stop_flag==0){//更新当前时间戳uv__update_time(loop);//执行定时器队列uv__run_timers(loop);//执行,因为最后一个循环没有执行,并且是I/O回调延迟到这个循环。ran_pending=uv__run_pending(循环);//内部调用,用户不关心,忽略uv__run_idle(loop);//内部调用,用户不关心,忽略uv__run_prepare(loop);超时=0;if((mode==UV_RUN_ONCE&&!ran_pending)||mode==UV_RUN_DEFAULT)//计算与下一个定时器到达的时间差。超时=uv_backend_timeout(循环);//进入轮询阶段,轮询I/O事件,有则执行,没有则阻塞,直到超过超时时间。uv__io_poll(循环,超时);//进入检查阶段,主要执行setImmediate回调。uv__run_check(循环);//执行close阶段,主要执行**close**事件uv__run_closing_handles(loop);if(mode==UV_RUN_ONCE){//更新当前时间戳uv__update_time(loop);//再次执行定时器回调。uv__run_timers(循环);}//判断当前事件循环是否存活。r=uv__loop_alive(loop);如果(模式==UV_RUN_ONCE||模式==UV_RUN_NOWAIT)中断;}/*if语句让gcc将其编译为条件存储。避免*弄脏缓存行。*/if(loop->stop_flag!=0)loop->stop_flag=0;returnr;}2.定时器阶段,源代码文件:timers.c。voiduv__run_timers(uv_loop_t*loop){structheap_node*heap_node;uv_timer_t*句柄;for(;;){//获取定时器堆中超时时间最接近的定时器句柄heap_node=heap_min((structheap*)&loop->timer_heap);如果(heap_node==NULL)中断;handle=container_of(heap_node,uv_timer_t,heap_node);//判断最新一个定时器句柄的超时时间是否大于当前时间,如果大于当前时间,说明还没有超时,跳出循环。如果(句柄->超时>循环->时间)中断;//停止最近的计时器句柄uv_timer_stop(handle);//判断定时器句柄类型是否为重复类型,如果是,则重新创建一个定时器句柄。uv_timer_again(句柄);//执行定时器句柄绑定的回调函数handle->timer_cb(handle);}}3.轮询阶段源码,源文件:kquene.cvoiduv__io_poll(uv_loop_t*loop,inttimeout){/*一系列变量初始化*///判断是否有事件发生if(loop->nfds==0){//判断watcher队列是否为空,如果为空则返回assert(QUEUE_EMPTY(&loop->watcher_queue));返回;}nevents=0;//watcher队列不为空while(!QUEUE_EMPTY(&loop->watcher_queue)){/*取出队列头部的watcher对象,取出watcher对象感兴趣的事件并监听。*/....省略一些代码w->events=w->pevents;}断言(超时>=-1);//如果超时,将当前时间赋值给基变量base=loop->time;//本轮最大监听事件数count=48;/*基准表明这提供了最佳的吞吐量。*///进入监控循环for(;;nevents=0){//如果超时,则初始化specif(timeout!=-1){spec.tv_sec=timeout/1000;spec.tv_nsec=(超时%1000)*1000000;}if(pset!=NULL)pthread_sigmask(SIG_BLOCK,pset,NULL);//监听内核事件,当事件到来时,返回事件数。//timeout为监听的超时时间,超时后返回。//我们知道超时是下一个传入的定时器到达的时间差,所以,在超时时间内,事件循环会阻塞在这里,直到超时或内核事件被触发。nfds=kevent(loop->backend_fd,events,nevents,events,ARRAY_SIZE(events),timeout==-1?NULL:&spec);如果(pset!=NULL)pthread_sigmask(SIG_UNBLOCK,pset,NULL);/*无条件更新循环->时间。当*超时==0(即非阻塞轮询)时跳过更新很诱人,但不能保证*操作系统不会在系统调用中重新安排我们的进程。*/SAVE_ERRNO(uv__update_time(loop));//如果内核没有监听到可用事件,并且本次监听超时,则返回。如果(nfds==0){断言(超时!=-1);返回;}if(nfds==-1){if(errno!=EINTR)abort();如果(超时==0)返回;如果(超时==-1)继续;/*被信号中断。更新超时并再次轮询。*/转到更新超时;..//判断事件循环的观察者队列是否为空assert(loop->watchers!=NULL);loop->watchers[loop->nwatchers]=(void*)事件;loop->watchers[loop->nwatchers+1]=(void*)(uintptr_t)nfds;//循环处理内核返回的事件,执行事件绑定回调函数for(i=0;i