Nodejs高性能原理(上) --- 異步非阻塞事件驅動模型

系列文章

Nodejs高性能原理(上) --- 異步非阻塞事件驅動模型
Nodejs高性能原理(下) --- 事件循環詳解html

前言

終於開始我nodejs的博客生涯了,先從基本的原理講起.之前寫過一篇瀏覽器執行機制的文章,和nodejs的類似之處仍是挺多的,不熟悉能夠去看看先.
Javascript執行機制--單線程,同異步任務,事件循環node

寫下來以後可能仍是有點懞,之後慢慢補充,也歡迎指正,特別是那篇翻譯文章後面已經看不懂了.有人出手科普一下就行了.由於懶得動手作,整篇文章的圖片要麼來源官網,要麼來源百度圖片.
補充: 當前Nodejs版本10.3.0
PS:
2019/8/13 修改部分描述內容linux

什麼是nodejs?

用官網的說法就是:
Node.js 是一個基於 Chrome V8 引擎的 JavaScript 運行環境。
Node.js 使用了一個事件驅動、非阻塞式 I/O 的模型,使其輕量又高效。
Node.js 的包管理器 npm,是全球最大的開源庫生態系統。
一三我就跳過不講了,那是外部條件因素,咱們集中精力瞭解第二條.git

什麼是非阻塞式 I/O?

摘抄自<<深刻淺出nodejs>>github

操做系統對計算機進行了抽象,將全部輸入輸出設備抽象爲文件.內核在進行文件I/O操做時,經過 文件描述符進行管理,而文件描述符相似於應用程序與系統內核之間的憑證.應用程序若是須要進行I/O調用,須要先打開文件描述符,而後再根據文件描述符去實現文件的數據讀寫. 此處非阻塞I/O與阻塞I/O的區別在於阻塞I/O完成整個獲取數據的過程,而非阻塞I/O則不帶數據直接返回,要獲取數據,還須要經過文件描述符再次讀取.

I/O是指磁盤文件系統或者數據庫的寫入和讀出,其中聽到一些名詞像異步,非阻塞,同步,阻塞之間好像是同一回事,實際效果而言又好像真的就是同一回事,可是從計算機內核I/O來講真不是同一回事,爲了更加全面講解這個點,咱們能夠把它們都列出來,分別是:數據庫

阻塞I/O(Blocking I/O)

在發起I/O操做以後會一直阻塞著進程不執行其餘操做,直到獲得響應或者超時爲止;
圖片描述
例子: 調用一個進行I/O操做的API請求時(如讀寫操做),必定要等待系統內核層面完成全部操做如磁盤尋道,讀取數據,複製數據到內存等等;
優勢: 基本不佔用 CPU 資源, 能保證操做結束或者數據返回;
缺點: 單進程單請求,阻塞形成CPU無謂的等待無法充分應用;npm

非阻塞I/O(Non-blocking I/O):

發起I/O操做不等獲得響應或者超時就當即返回讓進程繼續執行其餘操做;
圖片描述
例子: 調用一個進行I/O操做的API請求時(如讀寫操做),不等待系統內核層面完成全部操做如磁盤尋道,讀取數據,複製數據到內存等等就返回;
優勢: 提升性能減小等待時間;
缺點: 返回的僅僅是當前調用狀態,想要獲取完整數據須要重複去請求判斷操做是否完成形成CPU損耗,基本方法就是輪詢;segmentfault

I/O多路複用(I/O Multiplexing)

在併發量大的時候上面兩種確定都不適用,因而應運而生出多路複用,裏面又分幾種模式設計模式

select

將須要進行I/O操做的socket添加到select中進行監聽,而後阻塞線程,等待操做完成或超時以後select系統被激活調用返回,線程再發起I/O操做
具體方式仍是經過輪詢檢查全部的socket,由於單個進程支持的最大文件描述符是1024,因此實際併發量低於這個數
圖片描述
優勢: 同個線程能執行多個I/O,跨平臺支持
缺點: 原理上仍是屬於阻塞,單個I/O的處理時間甚至高過阻塞I/O,須要輪詢併發量有限(1024);瀏覽器

poll

同select機制相似,可是poll基於鏈表實現,併發量沒有限制
優勢: 同個線程能執行多個I/O,併發量沒有限制
缺點: 依然是遍歷鏈表檢查,效率低下;

epoll

針對前二者的缺點進行改進,經過callback回調通知機制.減小內存開銷,不因併發量大而下降效率,linux下最高效率的I/O事件機制
優勢: 同個線程能執行多個I/O,併發量遠遠超過1024且不影響性能
缺點: 併發量少的狀況下效率可能不如前二者;

信號驅動I/O(Signal-driven I/O)

應用程序使用socket進行信號驅動 I/O,並安裝一個信號處理函數,進程繼續運行並不阻塞。
當數據準備好時,進程會收到一個 SIGIO 信號,能夠在信號處理函數中調用 I/O 操做函數處理數據。
圖片描述
優勢: 執行以後不須要阻塞進程,當收到信號再執行操做提升資源利用
缺點: 併發量大的時候可能會由於信號隊列溢出致使無法通知;

同步I/O(Synchronous I/O):

發起I/O操做以後會阻塞進程直到獲得響應或者超時。按照這個定義,以前所述的阻塞I/O,非阻塞I/O,I/O多路複用, 信號驅動I/O都屬於同步I/O。
上面講的不論是等待完成全部操做仍是經過輪詢等方式獲取操做結果,其實都是會阻塞著進程,區別無非是中間等待時間怎麼分配;
優勢: 編寫執行順序一目瞭然;
缺點: 阻塞形成CPU無謂的等待或多餘的查詢,無法充分應用;

異步I/O(Asynchronous I/O):

直接返回繼續執行下一條語句,當I/O操做完成或數據返回時,以事件的形式通知執行IO操做的進程.
注意: 異步I/O跟信號驅動I/O除了同異步阻塞非阻塞的區別外,前者是通知進程I/O操做何時完成,後者是通知進程何時能夠發起I/O操做;
優勢: 提升性能無需等待或查詢,會有通知信息;
缺點: 代碼閱讀和流程控制較爲複雜;

圖片描述
(這裏本來想直接過,可是類似性過高容易模糊就打算畫圖,由於太多又懶得話想去百度找張圖,而後找不齊,最終在一個文章找到一個更加清晰明瞭的示意圖,很無恥又不失禮貌的借用了)
流程圖來自於IO - 同步,異步,阻塞,非阻塞 (亡羊補牢篇)

簡單總結:

阻塞I/O和非阻塞I/O區別在於:在I/O操做的完成或數據的返回前是等待仍是返回!(能夠理解成一直等仍是分時間段等)
同步I/O和異步I/O區別在於 :在I/O操做的完成或數據的返回前會不會將進程阻塞(或者說是主動查詢仍是被動等待通知)!

用個生活化的例子就是等外賣吧
阻塞I/O: 白領A下完單就守着前臺服務員直到收到外賣才離開,後面其餘人在排隊等他走開;
非阻塞I/O: 白領B下完單每隔一段時間就去詢問前臺服務員外賣好了沒,須要來回走屢次而且也要排隊可是妨礙其餘人的時間較少;
I/O多路複用: 白領A和B分別在兩個前臺服務員下單,廚房大叔先作好哪份外賣就交給對應的服務員;
信號驅動I/O: 白領C想要下單,前臺服務員先問問廚房還有沒有材料,獲得回覆以後再幫白領C下單;
異步I/O: 白領E下完單拿了號就去幹其餘事,直到前臺服務員叫號告訴他外賣好了;

爲何Nodejs這麼推崇非阻塞異步I/O?

用戶體驗

咱們都知道Javascript在瀏覽器中是單線程執行,JS引擎線程和GUI渲染線程是互斥的,若是你用同步方式加載資源的時候UI中止渲染,也不能進行交互,你猜用戶會幹嗎?
而使用異步加載的話就沒這問題了,這不只僅是阻塞期間的體驗問題,仍是加載時間的問題.

例若有兩段I/O代碼執行分別需時a和b,通常:
同步執行需時: a+b;
異步執行需時: Math.max(a,b);

這就是爲何異步非阻塞I/O是nodejs的主要理念,由於I/O代價很是昂貴.

資源分配

主流方法有兩種:

單線程串行依次執行

優勢: 編寫執行順序一目瞭然;
缺點: 沒法充分利用多核CPU;

多線程並行處理

優勢: 有效利用多核CPU;
缺點: 建立/切換線程開銷大,還有鎖,狀態同步等繁雜問題;

Nodejs方案:單線程事件驅動、非阻塞式 I/O

優勢: 免去鎖,狀態同步等繁雜問題,又能提升CPU利用率;

事件驅動

事件是一種經過監聽事件或狀態的變化而執行回調函數的流程控制方法,通常步驟

  1. 肯定響應事件的元素;
  2. 爲指定元素肯定須要響應的事件類型;
  3. 爲指定元素的指定事件編寫相應的事件處理程序;
  4. 將事件處理程序綁定到指定元素的指定事件;

咱們就以每一個入門必學的建立服務器爲例子

http
  .createServer((req, res) => {
    let data = '';
    req.on('data', chunk => (data += chunk));
    req.on('end', () => {
      res.end(data);
    });
  })
  .listen(8080);

所謂的事件驅動就是nodejs裏有個事件隊列,每一個進來的請求處理完就被關閉而後繼續服務下一個請求,當這個請求完成會被推動處理隊列,而後經過一種循環方式檢測隊列事件有沒變化,有就執行相對應的回調函數,沒有就跳過到下一步,如此往復.
圖片描述
(看看我在runoob看到的圖,一不當心又借用了.)
事件驅動很是高效可擴展性很是強,由於一直接受請求而不等待任何讀寫操做,更加詳細內容下面會講到.

nodejs的異步I/O實現

這塊知識點是從<<深刻淺出nodejs>>看到的.
四個共同構成Node異步I/O模型的基本要素:事件循環, 觀察者, 請求對象, 執行回調.
(由於涉及到底層語言和系統實現不一樣,我衹能根據內容簡單說說過程,再多無能爲力了)

事件循環

進程啓動以後node就會建立一個循環,每執行一次循環體的過程稱爲Tick.每一個Tick的過程就是看是否有事件待處理,有就取出事件及其相關回調執行,而後再重複Tick,不然退出進程.
圖片描述
(百度找到<<深刻淺出nodejs>>書本里的示意圖)

觀察者

Node.js 基本上全部的事件機制都是用設計模式中觀察者模式實現,每一個事件循環中有一個或多個的觀察者,經過詢問這些觀察者就能得知是否有事件須要進行處理.
瀏覽器中的事件可能來源於界面的交互或者文件加載而產生,而Node主要來源於網絡請求,文件I/O等,這些產生的事件都有對應的觀察者.
(window下基於IOCP建立,*nix基於多線程建立)

請求對象

對於Node中異步I/O調用,從發起調用到內核執行完I/O操做的過渡過程當中存在一種中間產物請求對象.
在Javascript層面代碼會調用C++核心模塊,核心模塊會調用內建模塊經過libuv進行系統調用.建立一個請求對象並將入參和當前方法等全部狀態都封裝在請求對象,包括送入線程池等待執行以及I/O操做完畢以後的回調處理.而後被推入線程池等待執行,Javascript調用至此返回繼續執行當前任務的後續操做,第一階段完成.

(官方介紹: libuv is a multi-platform support library with a focus on asynchronous I/O. It was primarily developed for use by Node.js,至關關鍵的東西)

圖片描述
(百度找到<<深刻淺出nodejs>>書本里的示意圖)

執行回調

線程池中的I/O操做調用完成以後會保存結果真後向IOCP(還記得上面說window下基於IOCP建立麼)提交執行狀態告知當前對象操做完成並將線程歸還線程池.中間還動用到事件循環的觀察者,每次Tick都會調用IOCP相關的方法檢查線程池是否有執行完的請求,有就將請求對象加入到I/O觀察者的隊列中看成事件處理.至此整個異步I/O流程結束.

完整流程以下

圖片描述
(百度找到<<深刻淺出nodejs>>書本里的示意圖)

參考資源

<<深刻淺出nodejs>>
runoob
IO - 同步,異步,阻塞,非阻塞 (亡羊補牢篇)

相關文章
相關標籤/搜索