Node異步

理解閉包

從形式來(lái)看,閉包就是在函數(shù)里面定義一個(gè)函數(shù),從特點(diǎn)來(lái)說,子函數(shù)能夠讀寫父函數(shù)的局部變量。

function parent() {
   var count = 0;
   return function children(){
      count++;
      console.log(count);
   }
}

var children = parent();
children();  // 1
children();  // 2

閉包能夠訪問外部函數(shù)的變量,在外部函數(shù)執(zhí)行完畢后,外部函數(shù)中的變量?jī)?nèi)存依然存在并未釋放,它的生命周期會(huì)保存到children變量?jī)?nèi)存被回收為止。要避免內(nèi)存泄漏,就要考慮何時(shí)注銷閉包函數(shù)的引用,理解它的生命周期,才能盡量避免可能產(chǎn)生的內(nèi)存泄漏。

所以要關(guān)注包含大對(duì)象的閉包函數(shù)對(duì)象,是否被引用到了root對(duì)象上,是否被注冊(cè)到事件循環(huán)中,是否對(duì)應(yīng)執(zhí)行了反注冊(cè)方法,是否置空,具體內(nèi)存接下來(lái)再花一篇來(lái)重點(diǎn)分析一下。

理解異步

我們?cè)诮佑|學(xué)習(xí)node時(shí)總會(huì)聽到node的單線程模型,其實(shí)這里會(huì)導(dǎo)致對(duì) Node.js的單線程會(huì)有個(gè)很深的誤會(huì)。事實(shí)上,這里的單線程指的是我們(開發(fā)者)編寫的代碼只能運(yùn)行在一個(gè)線程當(dāng)中(習(xí)慣稱之為主線程),Node.js并沒有給 Javascript 執(zhí)行時(shí)創(chuàng)建新線程的能力,所以稱為單線程,也就是所謂的主線程。 其實(shí),Nodejs中許多異步方法在具體的實(shí)現(xiàn)時(shí)(NodeJs底層封裝了Libuv,它提供了線程池、事件池、異步I/O等模塊功能,其完成了異步方法的具體實(shí)現(xiàn)),內(nèi)部均采用了多線程機(jī)制。

image.png

這里,主線程就是nodejs所謂的單線程,也就是用戶javascript代碼運(yùn)行的線程,I/O線程即執(zhí)行異步操作的線程。

image.png

執(zhí)行node app.js的流程如上圖所示:

1)node啟動(dòng),進(jìn)入main函數(shù);

2)初始化核心數(shù)據(jù)結(jié)構(gòu) default_loop_struct;這個(gè)數(shù)據(jù)結(jié)構(gòu)是事件循環(huán)的核心,當(dāng)node執(zhí)行到“加載js文件”時(shí),如果用戶的javascript代碼中具有異步IO操作時(shí),如讀寫文件。這時(shí)候,javascript代碼調(diào)用–>lib模塊–>C++模塊–>libuv接口–>最終系統(tǒng)底層的API,系統(tǒng)返回一個(gè)文件描述符fd 和javascript代碼傳進(jìn)來(lái)的回調(diào)函數(shù)callback,然后封裝成一個(gè)IO觀察者(一個(gè)uv__io_s類型的對(duì)象),保存到default_loop_struct。

3)加載用戶javascript文件,調(diào)用V8引擎接口,解析并執(zhí)行javascript代碼。如果有異步IO,則通過一系列調(diào)用系統(tǒng)底層API。

若是網(wǎng)絡(luò)IO,如http.get() 或者 app.listen() ,則把系統(tǒng)調(diào)用后返回的結(jié)果(文件描述符fd)和事件綁定的回調(diào)函數(shù)callback,一起封裝成一個(gè)IO觀察者,保存到default_loop_struct。

如果是文件IO,例如在uv_fs_open()的調(diào)用過程中,我們創(chuàng)建了一個(gè)FSReqWrap請(qǐng)求對(duì)象。從JavaScript層傳入的參數(shù)和當(dāng)前方法都被封裝在這個(gè)請(qǐng)求對(duì)象中,其中我們最為關(guān)心的回調(diào)函數(shù)則被設(shè)置在這個(gè)對(duì)象的oncomplete_sym屬性上:req_wrap->object_->Set(oncomplete_sym, callback)。對(duì)象包裝完畢后,在Windows下,則調(diào)用QueueUserWorkItem()方法將這個(gè)FSReqWrap對(duì)象推入線程池中等待執(zhí)行。。

至此,JavaScript調(diào)用立即返回,由JavaScript層面發(fā)起的異步調(diào)用的第一階段就此結(jié)束。JavaScript線程可以繼續(xù)執(zhí)行當(dāng)前任務(wù)的后續(xù)操作。當(dāng)前的I/O操作在線程池中等待執(zhí)行,不管它是否會(huì)阻塞I/O,都不會(huì)影響到JavaScript線程的后續(xù)執(zhí)行,如此就達(dá)到到了異步的目的。

等異步線程操作完畢,通知事件循環(huán)有異步io結(jié)束,需要調(diào)用回調(diào)函數(shù)。

4)進(jìn)入事件循環(huán),即調(diào)用libuv的事件循環(huán)入口函數(shù)uv_run();當(dāng)處理完 js代碼,如果有io操作,那么這時(shí)default_loop_struct是保存著對(duì)應(yīng)的io觀察者的。處理完js代碼,main函數(shù)繼續(xù)往下調(diào)用libuv的事件循環(huán)入口uv_run(),node進(jìn)程進(jìn)入事件循環(huán):

uv_run()的while循環(huán)做的就是一件事,判斷default_loop_struct是否有存活的io觀察者。 a. 如果沒有io觀察者,那么uv_run()退出,node進(jìn)程退出。 b. 而如果有io觀察者,那么uv_run()進(jìn)入epoll_wait(),線程掛起等待,監(jiān)聽對(duì)應(yīng)的io觀察者是否有數(shù)據(jù)到來(lái)。有數(shù)據(jù)到來(lái)調(diào)用io觀察者里保存著的callback(js代碼),沒有數(shù)據(jù)到來(lái)時(shí)一直在epoll_wait()進(jìn)行等待。

異步調(diào)用各線程流程圖及關(guān)系如下:

image.png

理解事件循環(huán)

事件循環(huán)的職責(zé),就是不斷得等待事件的發(fā)生,然后將這個(gè)事件的所有處理器,以它們訂閱這個(gè)事件的時(shí)間順序,依次執(zhí)行。當(dāng)這個(gè)事件的所有處理器都被執(zhí)行完畢之后,事件循環(huán)就會(huì)開始繼續(xù)等待下一個(gè)事件的觸發(fā),不斷往復(fù)。

Node.js采用V8作為js的解析引擎,而I/O處理方面使用了自己設(shè)計(jì)的libuv,libuv是一個(gè)基于事件驅(qū)動(dòng)的跨平臺(tái)抽象層,封裝了不同操作系統(tǒng)一些底層特性,對(duì)外提供統(tǒng)一的API,上文提到的事件循環(huán)機(jī)制是它里面的實(shí)現(xiàn),代碼如下:

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 callbacks階段
    ran_pending = uv__run_pending(loop);
    // idle階段
    uv__run_idle(loop);
    // prepare階段
    uv__run_prepare(loop);

    timeout = 0;
    if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
      timeout = uv_backend_timeout(loop);
    // poll階段
    uv__io_poll(loop, timeout);
    // check階段
    uv__run_check(loop);
    // close callbacks階段
    uv__run_closing_handles(loop);

    if (mode == UV_RUN_ONCE) {
      uv__update_time(loop);
      uv__run_timers(loop);
    }

    r = uv__loop_alive(loop);
    if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT)
      break;
  }

  if (loop->stop_flag != 0)
    loop->stop_flag = 0;

  return r;
}

每次事件循環(huán)都包含了6個(gè)階段,對(duì)應(yīng)上段代碼 libuv 源碼中的實(shí)現(xiàn)。

image.png
  • timers 階段

    timers 是事件循環(huán)的第一個(gè)階段,Node 會(huì)去檢查有無(wú)已過期的timer,如果有則把它的回調(diào)壓入timer的任務(wù)隊(duì)列中等待執(zhí)行,事實(shí)上,Node 并不能保證timer在預(yù)設(shè)時(shí)間到了就會(huì)立即執(zhí)行,因?yàn)镹ode對(duì)timer的過期檢查不一定靠譜,它會(huì)受機(jī)器上其它運(yùn)行程序影響,或者那個(gè)時(shí)間點(diǎn)主線程不空閑。

  • I/O callbacks 階段:執(zhí)行一些系統(tǒng)調(diào)用錯(cuò)誤,比如網(wǎng)絡(luò)通信的錯(cuò)誤回調(diào)。

  • idle, prepare 階段:僅node內(nèi)部使用。

  • poll 階段:獲取新的I/O事件, 適當(dāng)?shù)臈l件下node將阻塞在這里。

    主要有2個(gè)功能:

    • 處理 poll 隊(duì)列的事件
    • 當(dāng)有已超時(shí)的 timer,執(zhí)行它的回調(diào)函數(shù)

    在timers階段產(chǎn)生的超時(shí)回調(diào),在這個(gè)階段會(huì)執(zhí)行,直到超時(shí)timers隊(duì)列為空或執(zhí)行的回調(diào)達(dá)到系統(tǒng)上限(上限具體多少未詳)。接下來(lái)even loop會(huì)去檢查有無(wú)預(yù)設(shè)的setImmediate(),分兩種情況:若有預(yù)設(shè)的setImmediate(), event loop將結(jié)束poll階段進(jìn)入check階段,并執(zhí)行check階段的任務(wù)隊(duì)列。若沒有預(yù)設(shè)的setImmediate(),event loop將阻塞在該階段等待。

    這種阻塞狀態(tài)會(huì)被兩種情況打破,一個(gè)是timeout達(dá)到,一個(gè)是setImmediate方法執(zhí)行,這時(shí)候會(huì)進(jìn)入下一次loop循環(huán),重新檢查是否有超時(shí)的timers需要處理,進(jìn)入下一個(gè)消息循環(huán)。

  • check 階段:執(zhí)行 setImmediate() 的回調(diào)。

  • close callbacks 階段:執(zhí)行 socketclose 事件回調(diào)

所以為什么

const fs = require('fs')

fs.readFile('test.txt', () => {
  console.log('readFile')
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('immediate')
  })
})

的執(zhí)行結(jié)果是

readFile
immediate
timeout

因?yàn)閟etImmediate方法打破阻塞狀態(tài)優(yōu)先執(zhí)行check方法,而后才從超時(shí)隊(duì)列中取出超時(shí)timer回調(diào)執(zhí)行,再次進(jìn)入阻塞狀態(tài)。

注意上文中提到setTimeout并不是嚴(yán)格按照時(shí)間節(jié)點(diǎn)來(lái),如果在回調(diào)中執(zhí)行耗時(shí)的操作,導(dǎo)致下次消息循環(huán)觸發(fā)時(shí)間會(huì)整體延后,比如

var sleep = require('sleep');
setTimeout(() => {
    console.log('timeout')
}, 100);
setImmediate(() => {
    console.log('immediate')
    sleep.sleep(2);
})

則timeout的打印時(shí)間為2100秒以后,所以盡量不要在主線程中執(zhí)行耗時(shí)操作,耗時(shí)操作盡量都放在Worker線程中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容