代碼的藝術(shù) 卓越工程師必修課-好雨知時節(jié),當(dāng)春乃發(fā)生
徹底搞懂nodejs事情循環(huán)
代碼的藝術(shù) 卓越工程師必修課
download:https://www.zxit666.com/5852/
nodejs是單線程執(zhí)行的,同時它又是基于事情驅(qū)動的非阻塞IO編程模型。這就使得我們不用等候異步操作結(jié)果返回,就能夠繼續(xù)往下執(zhí)行代碼。當(dāng)異步事情觸發(fā)之后,就會通知主線程,主線程執(zhí)行相應(yīng)事情的回調(diào)。
以上是眾所周知的內(nèi)容。今天我們從源碼動手,剖析一下nodejs的事情循環(huán)機制。
nodejs架構(gòu)
首先,我們先看下nodejs架構(gòu),下圖所示:
如上圖所示,nodejs自上而下分為
- 用戶代碼 ( js 代碼 )
用戶代碼即我們編寫的應(yīng)用程序代碼、npm包、nodejs內(nèi)置的js模塊等,我們?nèi)粘9ぷ髦械拇缶植繒r間都是編寫這個層面的代碼。
- binding代碼或者三方插件(js 或 C/C++ 代碼)
膠水代碼,可以讓js調(diào)用C/C++的代碼。能夠?qū)⑵淞私鉃橐粋€橋,橋這頭是js,橋那頭是C/C++,經(jīng)過這個橋能夠讓js調(diào)用C/C++。
在nodejs里,膠水代碼的主要作用是把nodejs底層完成的C/C++庫暴露給js環(huán)境。
三方插件是我們本人完成的C/C++庫,同時需求我們本人完成膠水代碼,將js和C/C++停止橋接。
- 底層庫
nodejs的依賴庫,包括大名鼎鼎的V8、libuv。
V8: 我們都曉得,是google開發(fā)的一套高效javascript運轉(zhuǎn)時,nodejs可以高效執(zhí)行 js 代碼的很大緣由主要在它。
libuv:是用C言語完成的一套異步功用庫,nodejs高效的異步編程模型很大水平上歸功于libuv的完成,而libuv則是我們今天重點要剖析的。
還有一些其他的依賴庫
http-parser:擔(dān)任解析http響應(yīng)
openssl:加解密
c-ares:dns解析
npm:nodejs包管理器
...
關(guān)于nodejs不再過多引見,大家能夠自行查閱學(xué)習(xí),接下來我們重點要剖析的就是libuv。
libuv 架構(gòu)
我們曉得,nodejs完成異步機制的中心便是libuv,libuv承當(dāng)著nodejs與文件、網(wǎng)絡(luò)等異步任務(wù)的溝通橋梁,下面這張圖讓我們對libuv有個大約的印象:
這是libuv官網(wǎng)的一張圖,很明顯,nodejs的網(wǎng)絡(luò)I/O、文件I/O、DNS操作、還有一些用戶代碼都是在 libuv 工作的。
既然談到了異步,那么我們首先歸結(jié)下nodejs里的異步事情:
非I/O:
- 定時器(setTimeout,setInterval)
- microtask(promise)
- process.nextTick
- setImmediate
- DNS.lookup
I/O:
- 網(wǎng)絡(luò)I/O
- 文件I/O
- 一些DNS操作
- ...
網(wǎng)絡(luò)I/O
關(guān)于網(wǎng)絡(luò)I/O,各個平臺的完成機制不一樣,linux 是 epoll 模型,類 unix 是 kquene 、windows 下是高效的 IOCP 完成端口、SunOs 是 event ports,libuv 對這幾種網(wǎng)絡(luò)I/O模型停止了封裝。
文件I/O、異步DNS操作
libuv內(nèi)部還維護著一個默許4個線程的線程池,這些線程擔(dān)任執(zhí)行文件I/O操作、DNS操作、用戶異步代碼。當(dāng) js 層傳送給 libuv 一個操作任務(wù)時,libuv 會把這個任務(wù)加到隊列中。之后分兩種狀況:
- 1、線程池中的線程都被占用的時分,隊列中任務(wù)就要停止排隊等候閑暇線程。
- 2、線程池中有可用線程時,從隊列中取出這個任務(wù)執(zhí)行,執(zhí)行終了后,線程出借到線程池,等候下個任務(wù)。同時以事情的方式通知event-loop,event-loop接納到事情執(zhí)行該事情注冊的回調(diào)函數(shù)。
當(dāng)然,假如覺得4個線程不夠用,能夠在nodejs啟動時,設(shè)置環(huán)境變量UV_THREADPOOL_SIZE來調(diào)整,出于系統(tǒng)性能思索,libuv 規(guī)則可設(shè)置線程數(shù)不能超越128個。
nodejs源碼
先扼要引見下nodejs的啟動過程:
- 1、調(diào)用platformInit辦法 ,初始化 nodejs 的運轉(zhuǎn)環(huán)境。
- 2、調(diào)用 performance_node_start 辦法,對 nodejs 停止性能統(tǒng)計。
- 3、openssl設(shè)置的判別。
- 4、調(diào)用v8_platform.Initialize,初始化 libuv 線程池。
- 5、調(diào)用 V8::Initialize,初始化 V8 環(huán)境。
- 6、創(chuàng)立一個nodejs運轉(zhuǎn)實例。
- 7、啟動上一步創(chuàng)立好的實例。
- 8、開端執(zhí)行js文件,同步代碼執(zhí)行終了后,進入事情循環(huán)。
- 9、在沒有任何可監(jiān)聽的事情時,銷毀 nodejs 實例,程序執(zhí)行終了。
以上就是 nodejs 執(zhí)行一個js文件的全過程。接下來著重引見第八個步驟,事情循環(huán)。
我們看幾處關(guān)鍵源碼:
- 1、core.c,事情循環(huán)運轉(zhuǎn)的中心文件。
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; //判別事情循環(huán)能否存活。 r = uv__loop_alive(loop); //假如沒有存活,更新時間戳 if (!r) uv__update_time(loop); //假如事情循環(huán)存活,并且事情循環(huán)沒有中止。 while (r != 0 && loop->stop_flag == 0) { //更新當(dāng)前時間戳 uv__update_time(loop); //執(zhí)行 timers 隊列 uv__run_timers(loop); //執(zhí)行由于上個循環(huán)未執(zhí)行完,并被延遲到這個循環(huán)的I/O 回調(diào)。 ran_pending = uv__run_pending(loop); //內(nèi)部調(diào)用,用戶不care,疏忽 uv__run_idle(loop); //內(nèi)部調(diào)用,用戶不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事情,有則執(zhí)行,無則阻塞,直到超出timeout的時間。 uv__io_poll(loop, timeout); //進入check階段,主要執(zhí)行 setImmediate 回調(diào)。 uv__run_check(loop); //停止close階段,主要執(zhí)行 **關(guān)閉** 事情 uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { //更新當(dāng)前時間戳 uv__update_time(loop); //再次執(zhí)行timers回調(diào)。 uv__run_timers(loop); } //判別當(dāng)前事情循環(huán)能否存活。 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); // 判別最近的一個定時器句柄的超時時間能否大于當(dāng)前時間,假如大于當(dāng)前時間,闡明還未超時,跳出循環(huán)。 if (handle->timeout > loop->time) break; // 中止最近的定時器句柄 uv_timer_stop(handle); // 判別定時器句柄類型能否是repeat類型,假如是,重新創(chuàng)立一個定時器句柄。 uv_timer_again(handle); //執(zhí)行定時器句柄綁定的回調(diào)函數(shù) handle->timer_cb(handle); } }
- 3、 輪詢階段 源碼,源碼文件:kquene.c
void uv__io_poll(uv_loop_t* loop, int timeout) { /*一連串的變量初始化*/ //判別能否有事情發(fā)作 if (loop->nfds == 0) { //判別察看者隊列能否為空,假如為空,則返回 assert(QUEUE_EMPTY(&loop->watcher_queue)); return; } nevents = 0; // 察看者隊列不為空 while (!QUEUE_EMPTY(&loop->watcher_queue)) { /* 取出隊列頭的察看者對象 取出察看者對象感興味的事情并監(jiān)聽。 */ ....省略一些代碼 w->events = w->pevents; } assert(timeout >= -1); //假如有超時時間,將當(dāng)前時間賦給base變量 base = loop->time; // 本輪執(zhí)行監(jiān)聽事情的最大數(shù)量 count = 48; /* Benchmarks suggest this gives the best throughput. */ //進入監(jiān)聽循環(huán) 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); // 監(jiān)聽內(nèi)核事情,當(dāng)有事情到來時,即返回事情的數(shù)量。 // timeout 為監(jiān)聽的超時時間,超時時間一到即返回。 // 我們曉得,timeout是傳進來得下一個timers到來的時間差,所以,在timeout時間內(nèi),event-loop會不斷阻塞在此處,直到超時時間到來或者有內(nèi)核事情觸發(fā)。 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)); //假如內(nèi)核沒有監(jiān)聽到可用事情,且本次監(jiān)聽有超時時間,則返回。 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; } 。。。 //判別事情循環(huán)的察看者隊列能否為空 assert(loop->watchers != NULL); loop->watchers[loop->nwatchers] = (void*) events; loop->watchers[loop->nwatchers + 1] = (void*) (uintptr_t) nfds; // 循環(huán)處置內(nèi)核返回的事情,執(zhí)行事情綁定的回調(diào)函數(shù) for (i = 0; i < nfds; i++) { 。。。。 } }
參考 前端進階面試題細(xì)致解答
uv__io_poll階段源碼最長,邏輯最為復(fù)雜,能夠做個概括,如下:
當(dāng)js層代碼注冊的事情回調(diào)都沒有返回的時分,事情循環(huán)會阻塞在poll階段??吹竭@里,你可能會想了,會永遠(yuǎn)阻塞在此處嗎?
1、首先呢,在poll階段執(zhí)行的時分,會傳入一個timeout超時時間,該超時時間就是poll階段的最大阻塞時間。
2、其次呢,在poll階段,timeout時間未到的時分,假如有事情返回,就執(zhí)行該事情注冊的回調(diào)函數(shù)。timeout超時時間到了,則退出poll階段,執(zhí)行下一個階段。
所以,我們不用擔(dān)憂事情循環(huán)會永遠(yuǎn)阻塞在poll階段。
以上就是事情循環(huán)的兩個中心階段。限于篇幅,timers階段的其他源碼和setImmediate、process.nextTick的觸及到的源碼就不羅列了,感興味的童鞋能夠看下源碼。
最后,總結(jié)出事情循環(huán)的原理如下,以上你能夠不care,記住下面的總結(jié)就好了。
事情循環(huán)原理
node 的初始化
- 初始化 node 環(huán)境。
- 執(zhí)行輸入代碼。
- 執(zhí)行 process.nextTick 回調(diào)。
- 執(zhí)行 microtasks。
進入 event-loop
進入 timers 階段
- 檢查 timer 隊列能否有到期的 timer 回調(diào),假如有,將到期的 timer 回調(diào)依照 timerId 升序執(zhí)行。
- 檢查能否有 process.nextTick 任務(wù),假如有,全部執(zhí)行。
- 檢查能否有microtask,假如有,全部執(zhí)行。
- 退出該階段。
進入IO callbacks階段。
- 檢查能否有 pending 的 I/O 回調(diào)。假如有,執(zhí)行回調(diào)。假如沒有,退出該階段。
- 檢查能否有 process.nextTick 任務(wù),假如有,全部執(zhí)行。
- 檢查能否有microtask,假如有,全部執(zhí)行。
- 退出該階段。
進入 idle,prepare 階段:
- 這兩個階段與我們編程關(guān)系不大,暫且按下不表。
進入 poll 階段
首先檢查能否存在尚未完成的回調(diào),假如存在,那么分兩種狀況。
第一種狀況:
- 假如有可用回調(diào)(可用回調(diào)包含到期的定時器還有一些IO事情等),執(zhí)行一切可用回調(diào)。
- 檢查能否有 process.nextTick 回調(diào),假如有,全部執(zhí)行。
- 檢查能否有 microtaks,假如有,全部執(zhí)行。
- 退出該階段。
第二種狀況:
- 假如沒有可用回調(diào)。
- 檢查能否有 immediate 回調(diào),假如有,退出 poll 階段。假如沒有,阻塞在此階段,等候新的事情通知。
- 假如不存在尚未完成的回調(diào),退出poll階段。
進入 check 階段。
- 假如有immediate回調(diào),則執(zhí)行一切immediate回調(diào)。
- 檢查能否有 process.nextTick 回調(diào),假如有,全部執(zhí)行。
- 檢查能否有 microtaks,假如有,全部執(zhí)行。
- 退出 check 階段
進入 closing 階段。
- 假如有immediate回調(diào),則執(zhí)行一切immediate回調(diào)。
- 檢查能否有 process.nextTick 回調(diào),假如有,全部執(zhí)行。
- 檢查能否有 microtaks,假如有,全部執(zhí)行。
- 退出 closing 階段
檢查能否有活潑的 handles(定時器、IO等事情句柄)。
- 假如有,繼續(xù)下一輪循環(huán)。
- 假如沒有,完畢事情循環(huán),退出程序。
仔細(xì)的童鞋能夠發(fā)現(xiàn),在事情循環(huán)的每一個子階段退出之前都會按次第執(zhí)行如下過程:
- 檢查能否有 process.nextTick 回調(diào),假如有,全部執(zhí)行。
- 檢查能否有 microtaks,假如有,全部執(zhí)行。
- 退出當(dāng)前階段。
記住這個規(guī)律哦。