深刻淺出Node.js學習筆記(三)

異步I/O

在衆多高級編程語言或運行平臺中,Node是首個將異步做爲主要編程方式和設計理念。前端

Node的基調:異步I/O、事件驅動和單線程。編程

Nginx採用純C編寫。後端

Nginx具有面向客戶端鏈接的強大能力,但受限於各類同步的編程語言。數組

Node既能夠做爲服務器去處理客戶端的大量併發請求,也能夠做爲客戶端面向網絡中的各個應用進行併發請求。瀏覽器

1.爲何要異步I/O

1.1用戶體驗

前端經過異步能夠消除掉UI阻塞的現象,可是前端獲取資源的速度也取決於後端的響應速度。服務器

I/O是昂貴的,分佈式I/O是更昂貴的。網絡

只有後端可以快速響應資源,才能讓前端的體驗變好。多線程

1.2資源分配

假設業務場景中有一組互不相關的任務須要執行,主流的解決方案有:併發

  1. 單線程串行依次執行;
  2. 多線程並行完成;

添加硬件資源是一種提高服務質量的方式,但並非惟一的方式。異步

單線程同步編程模型會因阻塞I/O致使硬件資源得不到更優的使用;

多線程編程模型也由於編程中的死鎖、狀態同步等問題讓人詬病。

Node的解決方案:

  1. 利用單線程,遠離多線程死鎖、狀態同步等問題;
  2. 利用異步I/O,讓單線程遠離阻塞,以更好地使用CPU;

爲了彌補單線程沒法利用多核CPU的缺點,Node提供了相似前端瀏覽器中Web Works的子進程,該子進程能夠經過工做進程高效地利用CPU和I/O。

2.異步I/O實現現狀

2.1異步I/O和非阻塞I/O

操做系統內核對I/O只有兩種方式:

  1. 阻塞;
  2. 非阻塞;

阻塞I/O的特色:

  1. 調用以後必定要等到系統內核層完成全部的操做後,調用才結束;

阻塞I/O形成了CPU等待I/O,浪費等待時間,CPU的處理能力不能獲得充分利用。爲了提升性能,內核提供了非阻塞I/O。非阻塞I/O和阻塞I/O的差異爲調用以後會當即返回。

非阻塞I/O的缺點:

因爲完整的I/O並無完成,當即返回的並非業務層指望的數據,僅僅是當前調用的狀態。爲了獲取完整的數據,應用程序須要重複調用I/O操做來確認是否完成。(輪詢)

輪詢:

  1. read:最原始,性能最低的一種,經過重複調用來檢查I/O的狀態來完成完整數據的讀取;
  2. select:read的改進方案,經過對文件描述符上的事件狀態來判斷;
  3. poll:select的改進方案,採用鏈表的方式避免數組長度的限制,其次能避免不須要的檢查。可是當文件描述符較多的時候,性能依舊十分的低下;
  4. epoll:Linux下效率最高的I/O事件通知機制,在進入輪詢的時候若是沒有檢查到I/O事件,將會進行休眠,直到事件將它喚醒。真實利用了事件通知、執行回調的方式,而不是遍歷查詢,全部不會浪費CPU、執行效率較高;
  5. kqueue:與epoll相似,僅在FreeBSD系統存在;

輪詢對於應用程序而言只能算是一種同步。

2.2理想的非阻塞異步I/O

指望的完美的異步I/O應該是應用程序發起非阻塞調用,無須經過遍歷或者事件喚醒等方式輪詢,能夠直接處理下一個任務,只需在I/O完成後經過信號或回調將數據傳遞給應用程序。

2.3現實的異步I/O

多線程的異步I/O:

經過讓部分線程進行阻塞I/O或者非阻塞I/O加輪詢技術連完成數據獲取,讓一個進程進行計算處理,經過線程之間的通訊將I/O獲得的數據進行傳遞,實現異步I/O。

Windows的IOCP:

調用異步方法,等待I/O完成以後的通知,執行回調,用戶無須考慮輪詢,但內部是線程池的原理,不一樣之處在於這些線程池由系統內核接手管理。

Node的libuv:

Node提供了libuv做爲抽象封裝層,使得全部平臺兼容性的判斷都由這層來判斷,並保證上層的Node與下層的自定義的線程池及ICOP之間各類獨立。

3.Node的異步I/O

完成整個異步I/O環節的有事件循環、觀察者和請求對象等。

3.1事件循環

事件循環:

  1. 進程啓動,建立循環,每次執行循環體的過程稱爲Tick;
  2. Tick過程查看是否有事件待處理,若是有,就取出事件及其相關的回調函數;
  3. 若是存在關聯的回調函數,就執行;
  4. 進入下個循環,若是再也不有事件處理,就退出進程;

image

3.2觀察者

觀察者:

每一個事件循環有一個或者多個觀察者,而判斷是否有事件要處理的過程就是 向這些觀察者詢問是否有要處理的事件。

在Node中,事件主要來源於網絡請求、文件I/O等這些事件對應的觀察者都有文件I/O觀察者、網絡I/O觀察者。

觀察者將事件進行了分類,事件循環是個典型的生產者/消費者模型。異步I/O、網絡請求等是事件的生產者,源源不斷爲Node提供不一樣類型的事件,這些事件被傳遞到對應觀察者,事件循環從觀察者中提取事件並處理。

3.3請求對象

請求對象是異步I/O過程當中的重要中間產物,全部的狀態都保存在這個對象中,包括送入線程池等待執行以及I/O操做完畢後的回調處理。

3.4執行回調

I/O觀察者回調函數的行爲就是取出請求對象的result屬性做爲參數,取出oncomplete_sym屬性做爲方法,而後調用執行,以達到調用JavaScript中傳入的回調函數的目的。

整個I/O的流程:

image

事件循環、觀察者、請求對象、I/O線程池這四者共同構成了Node異步I/O模型的基本要素。

4.非I/O的異步API

非I/O的異步API:setTimeout()、setInerval()、process.nextTick()和setImmediate()。

4.1定時器

setTimeout()和setInerval()與瀏覽器中的API是一致的,分別用於單次和屢次定時執行任務。

它們的實現原理和異步I/O類似,只是不須要I/O線程池的參與。調用setTimeout()或者setInerval()建立的定時器會被插入到定時器觀察者內部的一個紅黑樹中。每次Tick執行時,會從該紅黑樹中迭代定時器對象,檢查是否超過定時時間,若是超過,就造成一個事件,它的回調函數將當即執行。

定時器的缺點:

定時器並不是是精確的。儘管事件循環十分快,可是若是某一次循環佔用的時間較多,那麼下次循環時,它也許超時好久了。

4.2process.nextTick()

每次調用process.nextTick()方法,只會將回調函數放入隊列中,在下一輪時取出執行。定時器中採用紅黑樹的操做時間複雜度爲O(lg(n)) ,nextTick () 的時間複雜度爲 O(1)。相比之下,process.nextTick()更加高效。

4.3setImmediate()

setImmediate()方法和process.nextTick()方法類似,都是將回調函數延遲執行。

可是,process.nextTick()中的回調函數執行的優先級要高於setImmediate()。

緣由在於事件循環對觀察者的檢查是有前後順序的,process.nextTick()屬於idle觀察者,setImmediate()屬於check觀察者。在每一輪循環檢查中,idle觀察者先於I/O觀察者,I/O觀察者先於check觀察者。

在具體實現上,process.nextTick()的回調函數保存在一個數組中,setImmediate()的結果保存在鏈表中;

在行爲上,process.nextTick()在每輪循環中會將數組中的回調函數所有執行完,而setImmediate()在每輪循壞中執行鏈表的每個回調函數。

5.事件驅動與高性能服務器

事件驅動的實質:

經過主循環加事件觸發的方式來運行程序。

利用Node構建Web服務器流程圖:

image

服務器模型對比:

  1. 同步式,一次只能處理一個請求,而且其他請求都處於等待狀態;
  2. 每進程/每請求,爲每一個請求啓動一個進程,能夠處理多個請求,不具有擴展性,由於系統資源有限;
  3. 每線程/每請求,爲每一個請求啓動一個線程來處理。儘管線程比進程輕量,可是因爲每一個線程都佔用必定內存,當大併發請求到來時,內存將會很快用光,致使服務器緩慢。

Node高性能的緣由:

  1. Node經過事件驅動的方式處理請求,無須爲每個請求建立額外的對應線程,能夠省掉建立線程和銷燬線程的開銷,同時操做系統在調度任務時由於線程較少,上下文的切換代價很低。
相關文章
相關標籤/搜索