node 異步 I/O

這篇文章主要講 nodejs 中的異步 IO,關于同步、異步、阻塞、非阻塞 請移步這里

事件循環 和 消息隊列

我們常說“JavaScript是單線程的”。

所謂單線程,是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個。不妨叫它主線程。

但是實際上還存在其他的線程。例如:處理AJAX請求的線程、定時器線程、讀寫文件的線程等等。這些線程可能存在于JS引擎之內,也可能存在于JS引擎之外,在此我們不做區分。不妨叫它們工作線程。

async_pic.png

node 執行過程

node_event.png

處理并執行完 js 代碼,main函數繼續往下調用libuv的事件循環入口uv_run(),node進程進入事件循環。 uv_run() 的 while 循環做的就是一件事,判斷 default_loop_struct 是否有存活的 io 觀察者 或 定時器。

事件循環

事件循環是指主線程重復從消息隊列中取消息、執行的過程

事件循環對應上圖 3 號標注的部分。用代碼表示大概是這樣的:

        while(true) {
            var message = queue.get();
            execute(message);
        }
event_loop.png

如上圖,每一次執行一次循環體的過程稱為 Tick。

事件循環的階段:

   ┌───────────────────────┐
┌─>│        timers         │ 執行定時器(setTimeout/setInterval)注冊的回調函數,也是進入事
│  └──────────┬────────────┘ 件循環第一個階段。
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │ I/O 事件相關聯的回調或者報錯會在這里執行
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │ 內部使用,不討論
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │ 最重要的一個階段,I/O 觀察者觀察到線程池
│  │         poll          │<─────┤  connections, │ 里有任務已經完成,就會在這里執行回調。
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │ 專門用來執行 setImmediate() 的回調
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │ 一個連接或 handle 突然被關閉,close 事件會被發送到這里執行回調
   └───────────────────────┘

如上圖,共有六個階段(官方稱為 phase)。特別要說明的是 poll 階段,在這個階段,如果暫時沒有事件到來,主線程便會阻塞在這里,等待事件發生。當然它不會一直等下去:

  • 它首先會判斷后面的 Check Phase 以及 Close Phase 是否還有等待處理的回調. 如果有, 則不等待, 直接進入下一個 Phase.
  • 如果沒有其他回調等待執行, 它會給 epoll 這樣的方法設置一個 timeout. 可以猜一下, 這個 timeout 設置為多少合適呢? 答案就是 Timer Phase 中最近要執行的回調啟動時間到現在的差值, 假設這個差值是 delta. 因為 Poll Phase 后面沒有等待執行的回調了. 所以這里最多等待 delta 時長, 如果期間有事件喚醒了消息循環, 那么就繼續下一個 Phase 的工作; 如果期間什么都沒發生, 那么到了 timeout 后, 消息循環依然要進入后面的Phase, 讓下一個迭代的 Timer Phase 也能夠得到執行.

來看一下流程:

phases.png

到這里你一定發現少了一些問題:process.nextTick() 和 Promise 都是異步的,它們對應以上哪個階段呢?往下看

任務隊列

1、運行主線程(函數調用棧)中的同步任務
2、主線程(函數調用棧)執行到任務源時,通知相應的webAPIs進行相應的執行異步任務,將任務源指定的異步任務放入任務隊列中
3、主線程(函數調用棧)中的任務執行完畢后,然后執行所有的微任務,再執行宏任務,找到一個任務隊列執行完畢,再執行所有的微任務
4、不斷執行第三步

任務隊列也叫消息隊列。主要分兩類任務:宏任務(macro-task)、微任務(micro-task)

宏任務:setTimeout setInterval setImmediate I/O

微任務:process.nextTick Promise 的回調

在上面的圖中,各個 phase 完成了宏任務對應的事件。微任務的執行時機在每一次進入下一個階段之前,process.nextTick 優先級大于 Promise 的回調。

FAQ

setTimeout 和 setImmediate 的比較
setImmediate(() => console.log(2))
setTimeout(() => console.log(1))

這段代碼的結果實際上是不確定的。可是,為什么?按照流程圖,應該是 timer 先于 check 階段,所以應該是 setTimeout 先執行,可是為什么結果不是這樣呢?首先我們要知道:

setTimeout(fn) ==> setTimeout(fn, 0) ==> setTimeout(fn, 1)

上面三個效果是一樣的!前兩個好理解,給定的默認值是0。其實在 node 源碼中,最低為 1 ms,官方文檔如下:

When delay is larger than 2147483647 or less than 1, the delay will be set to 1.

所以當進入 timer 階段時,1ms 可能超時也可能沒有,這個影響因素有很多。如果還沒超時,則進入下一個 phase,依次往下,所以先輸出 2 。如果已經超時,則先輸出 1。

但是!如果它們在 I/O 事件回調中,那么輸出順序是固定了的,如下

require('fs').readFile('path.txt', () => {
 setImmediate(() => console.log(2))
 setTimeout(() => console.log(1))
});
// 輸出: 2 1

如果不知道為什么,答案就在循環圖中。

(完)

Reference

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 單線程編程會因阻塞I/O導致硬件資源得不到更優的使用。多線程編程也因為編程中的死鎖、狀態同步等問題讓開發人員頭痛。...
    exialym閱讀 451評論 0 1
  • 讓I/O與CPU計算并行 Node 在*nix平臺,通過線程池實現(主線程和I/O線程),在windows下使用I...
    wmtcore閱讀 374評論 0 0
  • 參考:IOCP原理Philip Roberts: Help, I’m stuck in an event-loop...
    mary_s閱讀 2,251評論 1 8
  • 一、JavaScript單線程模型 JavaScript是單線程的,JavaScript只在一個線程上運行,但是瀏...
    Brolly閱讀 1,156評論 4 6
  • 如果你是一枚硬幣 一面花朵一面你的雕塑 在旅途中跟隨 在我的左口袋或右口袋跳躍 親愛的 我的手心便都是你 午后的陽...
    冬日皚皚閱讀 251評論 0 2