node.js的異步I/O、事件驅動、單線程

nodejs的特色總共有如下幾點html

  1. 異步I/O(非阻塞I/O)
  2. 事件驅動
  3. 單線程
  4. 擅長I/O密集型,不擅長CPU密集型
  5. 高併發

下面是一道很經典的面試題,描述了node的總體運行機制,相信不少人都碰到了。這道題背後的原理就是nodejs代碼執行順序java

setTimeout(function() {
        console.log('4');
    },0)

    setImmediate(function() {
        console.log('5');
    })

    let s = new Promise(function(resolve, reject) {
        console.log('2');
        resolve(true)
        console.log('7')
    })

    s.then(function() {
        console.log('3');
    })

    process.nextTick(function() {
        console.log('6')
    })

    console.log('1');
    // 我電腦的輸出結果是 二、七、一、六、三、四、5

1. nodejs代碼執行順序(事件循環機制)

nodejs的運行機制: nodejs主線程主要起一個任務調度的做用。nodejs用一個主線程處理全部的請求, 將I/O操做交由底下的線程池處理;在全部主線程任務執行完成後,主線程處理事件隊列。 因此在同步初始化代碼執行完成後,nodejs會基於事件隊列不停的作事件循環。事實上,nodejs運行環境 = 主線程(單線程,包括事件隊列) + 線程池(工做線程池,執行其餘工做-多線程)node

  • node 的初始化
    • 初始化 node 環境。
    • 執行輸入代碼。
    • 執行 process.nextTick 回調。
    • 執行 microtasks。(Promise.then)
  • 進入事件循環
    • 進入 timers 階段 (定時器階段:本階段執行已經安排的 setTimeout() 和 setInterval() 的回調函數。)
      • 檢查 timer 隊列是否有到期的 timer 回調,若是有,將到期的 timer 回調按照 timerId 升序執行。
      • 檢查是否有 process.nextTick 任務,若是有,所有執行。
      • 檢查是否有microtask,若是有,所有執行。
      • 退出該階段。
    • 進入pending IO callbacks階段。(對某些系統操做(如 TCP 錯誤類型)執行回調)
      • 檢查是否有 pending 的 I/O 回調。若是有,執行回調。若是沒有,退出該階段。
      • 檢查是否有 process.nextTick 任務,若是有,所有執行。
      • 檢查是否有microtask,若是有,所有執行。
      • 退出該階段。
    • 進入 idle,prepare 階段:
      • 僅系統內部使用。
    • 進入 poll 階段(檢索新的 I/O 事件;執行與 I/O 相關的回調,除了定時器和關閉的回調函數,其他都在這裏)
      • 首先檢查是否存在還沒有完成的回調,若是存在,那麼分兩種狀況。
        • 第一種狀況:
          • 若是有可用回調(可用回調包含到期的定時器還有一些IO事件等),執行全部可用回調。
          • 檢查是否有 process.nextTick 回調,若是有,所有執行。
          • 檢查是否有 microtaks,若是有,所有執行。
          • 退出該階段。
        • 第二種狀況:
          • 若是沒有可用回調,執行下一步;
          • 檢查是否有 immediate 回調,若是有,退出 poll 階段。若是沒有,阻塞在此階段,等待新的事件通知。
          • 若是不存在還沒有完成的回調,退出poll階段。
    • 進入 check 階段。(setImmediate() 回調函數在這裏執行)
      • 若是有immediate回調,則執行全部immediate回調。
      • 檢查是否有 process.nextTick 回調,若是有,所有執行。
      • 檢查是否有 microtaks,若是有,所有執行。
      • 退出 check 階段
    • 進入 closing 階段。(檢測關閉的回調函數,例如 xx.on('close'))
      • 若是有immediate回調,則執行全部immediate回調。
      • 檢查是否有 process.nextTick 回調,若是有,所有執行。
      • 檢查是否有 microtaks,若是有,所有執行。
      • 退出 closing 階段
        • 檢查是否有活躍的 handles(定時器、IO等事件句柄)。
          • 若是有,繼續下一輪循環。
          • 若是沒有,結束事件循環,退出程序。

: 在主線程執行完和事件循環總共7個階段,每個階段執行完都會調用一遍process.nextTick回調,一遍microtaks(promise);git

2. setImmediate和process.nextTick和setTimeout

  • setImmediate(): 事件循環poll階段執行完後執行setImmediate;
  • process.nextTick():主線程和事件循環每一階段完成後都會調用;
  • setTimeout(): 最少通過n毫秒後執行的腳本,受到前一次事件循環時間影響,實際執行時間爲>=n毫秒
  • ** setTimeout和setImmediate執行順序問題**
    • 若是運行的是不屬於 I/O 週期(即主模塊)的如下腳本,則執行兩個計時器的順序是非肯定性的,由於它受進程性能的約束;
    • 若是你把這兩個函數放入一個 I/O 循環內調用,setImmediate 老是被優先調用;I/O場景推薦使用setsetImmediate,由於setsetImmediate始終並且是當即執行

3. 對上題的理解

主線程中,console.logpromise的new方法在初始化主線程中執行,他們倆個的輸出時間按照先上後下的順序輸出,他們兩個執行完後會當即執行主線程的process.nextTick,而後執行promise.then方法,而後是進入事件隊列中執行setTimeoutsetImmediate。由於setTimeout的
'最少通過n毫秒後執行的腳本'特性,致使沒法肯定setTimeoutsetImmediate的執行前後順序,但若是是在回調函數中,則必然setImmediate先執行,由於事件循環的階段中,setImmediate緊挨着回調函數以後執行,而setTimeout則在下次事件循環中執行。github

4. 單線程和多線程

  • 多線程: 服務器爲每一個客戶端請求分配一個線程,使用同步 I/O,系統經過線程切換來彌補同步 I/O 調用的時間開銷。好比 Apache 就是這種策略,因爲 I/O 通常都是耗時操做,所以這種策略很難實現高性能,但很是簡單,能夠實現複雜的交互邏輯。
  • 單線程: 而事實上,大多數網站的服務器端都不會作太多的計算,它們接收到請求之後,把請求交給其它服務來處理(好比讀取數據庫),而後等着結果返回,最後再把結果發給客戶端。所以,Node.js 針對這一事實採用了單線程模型來處理,它不會爲每一個接入請求分配一個線程,而是用一個主線程處理全部的請求,而後對 I/O 操做進行異步處理,避開了建立、銷燬線程以及在線程間切換所需的開銷和複雜性。

5. 異步I/O

  • IO操做: IO操做就是以流的形式,進行的操做,好比網絡請求,文件讀取寫入。IO操做也就是input和output的操做。web

  • 阻塞IO: 在調用阻塞O時,應用程序須要等待IO完成才能返回結果。 阻塞IO的特色:調用以後必定要等到系統內核層面完成全部操做以後,調用才結束。 阻塞O形成CUP等待IO,浪費等待時間,CPU的處理能力不能獲得充分利用。面試

  • 非阻塞IO: 爲了提升性能,內核提供了非阻塞IO,非阻塞IO跟阻塞IO的差異是調用以後會當即返回。阻塞IO完成整個獲取數據的過程,而非阻塞IO則不帶數據直接返回,要獲取數據,還要經過描述符再次讀取。非阻塞IO返回以前,node主線程能夠用來處理其餘事物,此時性能提高很是明顯。數據庫

  • 爲何node擅長I/O密集型,不擅長CPU密集型:由於node的I/O處理中主線程只負責轉發,實際操做在其餘線程及線程隊列裏完成,因此性能相對較高; 而CPU密集則要求node的主線程處理,這時候其他請求只能等待promise

  • 個人理解: node的異步I/O分爲兩個階段,第一個階段是主線程調用線程池裏的工做線程執行異步操做,主線程取回對應的描述符,存儲下來,工做線程執行相關操做取回數據後存儲下來,這一部分在主線程接收到請求後當即完成;第二個階段在事件隊列裏完成,根據描述符去工做線程裏去獲取數據,以提高性能.安全

6. 高併發

如下是對nodejs高併發的理解,nodejs的高併發體如今處理I/O的性能上,而不是CPU密集上,摘錄自官網文檔

讓咱們思考這樣一種狀況:每一個對 Web 服務器的請求須要 50 毫秒完成,而那 50 毫秒中的 45 毫秒是能夠異步執行的數據庫 I/O。選擇 非阻塞 異步操做能夠釋放每一個請求的 45 毫秒來處理其它請求。僅僅是選擇使用 非阻塞 方法而不是 阻塞 方法,就是容量上的重大區別。

7. 總結

Node 有兩種類型的線程:一個事件循環線程和 k 個工做線程。 事件循環負責 JavaScript 回調和非阻塞 I/O,工做線程執行與 C++ 代碼對應的、完成異步請求的任務,包括阻塞 I/O 和 CPU 密集型工做。 這兩種類型的線程一次都只能處理一個活動。 若是任意一個回調或任務須要很長時間,則運行它的線程將被 阻塞。 若是你的應用程序發起阻塞的回調或任務,在好的狀況下這可能只會致使吞吐量降低(客戶端/秒),而在最壞狀況下可能會致使徹底拒絕服務。要編寫高吞吐量、防 DoS 攻擊的 web 服務,您必須確保無論在良性或惡意輸入的狀況下,您的事件循環線程和您的工做線程都不會阻塞。

一般意義上,I/O密集型活動,如網絡I/O、文件I/O,DNS操做等一般建議放在對外提供網絡服務的端口所在的服務內,剩下的諸如大內容的crypto,zlib,fs同步操做、子進程,JSON處理、計算等儘可能另起node服務或者其餘語言服務去進行,由於這些操做會影響到node的主線程的性能和安全性。

參考

  1. Node.js 事件循環機制
  2. nodejs筆記之:事件驅動,線程池,非阻塞,異常處理等
  3. 官網文檔
  4. Node.js 事件循環,定時器和 process.nextTick()
  5. nodejs 事件循環
  6. 不要阻塞你的事件循環(或是工做線程池

題外話

事實上,對於nodejs的相關理解更多的收穫在於這裏,nodejs官網指南的中文文檔,之前有點粗心了

相關文章
相關標籤/搜索