彻底搞懂nodejs事情循环
代码的艺术 卓越工程师必修课
download:https://www.zxit666.com/5852/
(资料图)
nodejs是单线程执行的,同时它又是基于事情驱动的非阻塞IO编程模型。这就使得我们不用等候异步操作结果返回,就能够继续往下执行代码。当异步事情触发之后,就会通知主线程,主线程执行相应事情的回调。
以上是众所周知的内容。今天我们从源码动手,剖析一下nodejs的事情循环机制。
首先,我们先看下nodejs架构,下图所示:
如上图所示,nodejs自上而下分为
用户代码 ( js 代码 )用户代码即我们编写的应用程序代码、npm包、nodejs内置的js模块等,我们日常工作中的大局部时间都是编写这个层面的代码。binding代码或者三方插件(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。
我们晓得,nodejs完成异步机制的中心便是libuv,libuv承当着nodejs与文件、网络等异步任务的沟通桥梁,下面这张图让我们对libuv有个大约的印象:
这是libuv官网的一张图,很明显,nodejs的网络I/O、文件I/O、DNS操作、还有一些用户代码都是在 libuv 工作的。既然谈到了异步,那么我们首先归结下nodejs里的异步事情:
非I/O:
定时器(setTimeout,setInterval)microtask(promise)process.nextTicksetImmediateDNS.lookupI/O:
网络I/O文件I/O一些DNS操作...关于网络I/O,各个平台的完成机制不一样,linux 是 epoll 模型,类 unix 是 kquene 、windows 下是高效的 IOCP 完成端口、SunOs 是 event ports,libuv 对这几种网络I/O模型停止了封装。
libuv内部还维护着一个默许4个线程的线程池,这些线程担任执行文件I/O操作、DNS操作、用户异步代码。当 js 层传送给 libuv 一个操作任务时,libuv 会把这个任务加到队列中。之后分两种状况:
1、线程池中的线程都被占用的时分,队列中任务就要停止排队等候闲暇线程。2、线程池中有可用线程时,从队列中取出这个任务执行,执行终了后,线程出借到线程池,等候下个任务。同时以事情的方式通知event-loop,event-loop接纳到事情执行该事情注册的回调函数。当然,假如觉得4个线程不够用,能够在nodejs启动时,设置环境变量UV_THREADPOOL_SIZE来调整,出于系统性能思索,libuv 规则可设置线程数不能超越128个。
先扼要引见下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,事情循环运转的中心文件。int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending;//判别事情循环能否存活。 r = uv__loop_alive(loop); //假如没有存活,更新时间戳 if (!r) uv__update_time(loop);//假如事情循环存活,并且事情循环没有中止。 while (r != 0 && loop->stop_flag == 0) { //更新当前时间戳 uv__update_time(loop); //执行 timers 队列 uv__run_timers(loop); //执行由于上个循环未执行完,并被延迟到这个循环的I/O 回调。 ran_pending = uv__run_pending(loop); //内部调用,用户不care,疏忽 uv__run_idle(loop); //内部调用,用户不care,疏忽 uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) //计算间隔下一个timer到来的时间差。 timeout = uv_backend_timeout(loop); //进入 轮询 阶段,该阶段轮询I/O事情,有则执行,无则阻塞,直到超出timeout的时间。 uv__io_poll(loop, timeout); //进入check阶段,主要执行 setImmediate 回调。 uv__run_check(loop); //停止close阶段,主要执行 **关闭** 事情 uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { //更新当前时间戳 uv__update_time(loop); //再次执行timers回调。 uv__run_timers(loop); } //判别当前事情循环能否存活。 r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } /* The if statement lets gcc compile it to a conditional store. Avoids * dirtying a cache line. */ if (loop->stop_flag != 0) loop->stop_flag = 0; return r;}
2、timers 阶段,源码文件:timers.c。void uv__run_timers(uv_loop_t* loop) { struct heap_node* heap_node; uv_timer_t* handle; for (;;) { //取出定时器堆中超时时间最近的定时器句柄 heap_node = heap_min((struct heap*) &loop->timer_heap); if (heap_node == NULL) break; handle = container_of(heap_node, uv_timer_t, heap_node); // 判别最近的一个定时器句柄的超时时间能否大于当前时间,假如大于当前时间,阐明还未超时,跳出循环。 if (handle->timeout > loop->time) break; // 中止最近的定时器句柄 uv_timer_stop(handle); // 判别定时器句柄类型能否是repeat类型,假如是,重新创立一个定时器句柄。 uv_timer_again(handle); //执行定时器句柄绑定的回调函数 handle->timer_cb(handle); }}
3、 轮询阶段 源码,源码文件:kquene.cvoid uv__io_poll(uv_loop_t* loop, int timeout) { /*一连串的变量初始化*/ //判别能否有事情发作 if (loop->nfds == 0) { //判别察看者队列能否为空,假如为空,则返回 assert(QUEUE_EMPTY(&loop->watcher_queue)); return; } nevents = 0; // 察看者队列不为空 while (!QUEUE_EMPTY(&loop->watcher_queue)) { /* 取出队列头的察看者对象 取出察看者对象感兴味的事情并监听。 */ ....省略一些代码 w->events = w->pevents; } assert(timeout >= -1); //假如有超时时间,将当前时间赋给base变量 base = loop->time; // 本轮执行监听事情的最大数量 count = 48; /* Benchmarks suggest this gives the best throughput. */ //进入监听循环 for (;; nevents = 0) { // 有超时时间的话,初始化spec if (timeout != -1) { spec.tv_sec = timeout / 1000; spec.tv_nsec = (timeout % 1000) * 1000000; } if (pset != NULL) pthread_sigmask(SIG_BLOCK, pset, NULL); // 监听内核事情,当有事情到来时,即返回事情的数量。 // timeout 为监听的超时时间,超时时间一到即返回。 // 我们晓得,timeout是传进来得下一个timers到来的时间差,所以,在timeout时间内,event-loop会不断阻塞在此处,直到超时时间到来或者有内核事情触发。 nfds = kevent(loop->backend_fd, events, nevents, events, ARRAY_SIZE(events), timeout == -1 ? NULL : &spec); if (pset != NULL) pthread_sigmask(SIG_UNBLOCK, pset, NULL); /* Update loop->time unconditionally. It's tempting to skip the update when * timeout == 0 (i.e. non-blocking poll) but there is no guarantee that the * operating system didn't reschedule our process while in the syscall. */ SAVE_ERRNO(uv__update_time(loop)); //假如内核没有监听到可用事情,且本次监听有超时时间,则返回。 if (nfds == 0) { assert(timeout != -1); return; } if (nfds == -1) { if (errno != EINTR) abort(); if (timeout == 0) return; if (timeout == -1) continue; /* Interrupted by a signal. Update timeout and poll again. */ goto update_timeout; } 。。。 //判别事情循环的察看者队列能否为空 assert(loop->watchers != NULL); loop->watchers[loop->nwatchers] = (void*) events; loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds; // 循环处置内核返回的事情,执行事情绑定的回调函数 for (i = 0; i < nfds; i++) { 。。。。 }}
参考 前端进阶面试题细致解答
uv__io_poll阶段源码最长,逻辑最为复杂,能够做个概括,如下:当js层代码注册的事情回调都没有返回的时分,事情循环会阻塞在poll阶段。看到这里,你可能会想了,会永远阻塞在此处吗?
1、首先呢,在poll阶段执行的时分,会传入一个timeout超时时间,该超时时间就是poll阶段的最大阻塞时间。2、其次呢,在poll阶段,timeout时间未到的时分,假如有事情返回,就执行该事情注册的回调函数。timeout超时时间到了,则退出poll阶段,执行下一个阶段。
所以,我们不用担忧事情循环会永远阻塞在poll阶段。
以上就是事情循环的两个中心阶段。限于篇幅,timers阶段的其他源码和setImmediate、process.nextTick的触及到的源码就不罗列了,感兴味的童鞋能够看下源码。
最后,总结出事情循环的原理如下,以上你能够不care,记住下面的总结就好了。
node 的初始化
初始化 node 环境。执行输入代码。执行 process.nextTick 回调。执行 microtasks。进入 event-loop
进入 timers 阶段
检查 timer 队列能否有到期的 timer 回调,假如有,将到期的 timer 回调依照 timerId 升序执行。检查能否有 process.nextTick 任务,假如有,全部执行。检查能否有microtask,假如有,全部执行。退出该阶段。进入IO callbacks阶段。
检查能否有 pending 的 I/O 回调。假如有,执行回调。假如没有,退出该阶段。检查能否有 process.nextTick 任务,假如有,全部执行。检查能否有microtask,假如有,全部执行。退出该阶段。进入 idle,prepare 阶段:
这两个阶段与我们编程关系不大,暂且按下不表。进入 poll 阶段
首先检查能否存在尚未完成的回调,假如存在,那么分两种状况。
第一种状况:
假如有可用回调(可用回调包含到期的定时器还有一些IO事情等),执行一切可用回调。检查能否有 process.nextTick 回调,假如有,全部执行。检查能否有 microtaks,假如有,全部执行。退出该阶段。第二种状况:
假如没有可用回调。检查能否有 immediate 回调,假如有,退出 poll 阶段。假如没有,阻塞在此阶段,等候新的事情通知。假如不存在尚未完成的回调,退出poll阶段。进入 check 阶段。
假如有immediate回调,则执行一切immediate回调。检查能否有 process.nextTick 回调,假如有,全部执行。检查能否有 microtaks,假如有,全部执行。退出 check 阶段进入 closing 阶段。
假如有immediate回调,则执行一切immediate回调。检查能否有 process.nextTick 回调,假如有,全部执行。检查能否有 microtaks,假如有,全部执行。退出 closing 阶段检查能否有活泼的 handles(定时器、IO等事情句柄)。
假如有,继续下一轮循环。假如没有,完毕事情循环,退出程序。仔细的童鞋能够发现,在事情循环的每一个子阶段退出之前都会按次第执行如下过程:
检查能否有 process.nextTick 回调,假如有,全部执行。检查能否有 microtaks,假如有,全部执行。退出当前阶段。记住这个规律哦。
上一篇:世界快讯:墙绘机器人_墙绘机
下一篇:最后一页
2022年5月13日21时52分许,郑州交警二支队二大队大队长马大鹏、副大队长张志良带领民警在南三环与工人路交叉口开展酒驾查处行动时,查到一
5月18日,记者从郑州交管局获悉,近日,郑州交警在查处非机动车、电动车等交通违法时,通过依法依规处罚、现场学习交通法规,扫码下载朗读
据国家统计局河南调查总队数据显示,4月份全省居民消费价格同比上涨1 6%,涨幅比上月扩大0 6个百分点;1-4月,全省居民消费价格同比上涨0 8
5月18日,针对带电动调节功能转向柱减配事件,保时捷中国公布解决方案。保时捷中国方面表示,针对此前承诺过恢复转向柱电动调节功能的客户
医护人员不用穿成大白,呆在凉爽的小屋里就能完成核酸采样;而市民也不用担心飞沫传染,就近来到小屋前就能迅速进行核酸检测。5月17日上午
X 关闭
X 关闭