官术网_书友最值得收藏!

1.2 Node的內部機制

本節的內容會涉及一些操作系統的概念,在開始之前,這里有一些前提,記住這些前提會能讓你更好地理解本節的內容:

  • 在任務完成之前,CPU在任何情況下都不會暫停或者停止執行,CPU如何執行和同步或是異步、阻塞或者非阻塞都沒有必然關系。
  • 操作系統始終保證CPU處在運行狀態,這是通過系統調度來實現的,具體一點就是通過在不同進程/線程間切換實現的。

1.2.1 何為回調

1.回調的定義

一個回調是指通過函數參數的參數傳遞到其他代碼的,某段可執行代碼的引用。

說得通俗一點,就是將一個函數作為參數傳遞給另一個函數,并且作為參數的函數可以被執行,其本質上是一個高階函數。在數學和計算機科學中,高階函數是至少滿足下列一個條件的函數:

  • 接受一個或多個函數作為輸入。
  • 輸出一個函數。

JavaScript中一個很常見的例子就是map方法,該方法接受一個函數作為參數,依次作用于的數組的每一個元素。

可以用如圖1-1所示來描述回調的過程。

圖1-1

回調方法和主線程處于同一層級,假設主線程發起了一個底層的系統調用,那么操作系統轉而去執行這個系統調用,當調用結束后,又回到主線程上調用其后的方法,這也是為什么其會被稱為回調(call then back)。

關于回調函數在何時執行并沒有具體的要求,回調函數的調用既可以是同步的(例如map方法),也可以是異步的(例如setTimeout方法中的匿名函數)。

2.異步過程中的回調

單線程運行的語言在設計時要考慮這樣的問題:如果遇到一個耗時的操作,例如磁盤IO,要不要等待操作完成了再執行下一步操作?

有的語言選擇了在完成之前繼續等待,例如PHP。

Node選擇另一種方式,當遇到IO操作時,Node代碼在發起一個調用后繼續向下執行,IO操作完成后,再執行對應的回調函數(異步),雖然代碼運行在單線程環境下,但依靠異步+回調的方式,也能實現對高并發的支持。

代碼1.1 回調函數示意

代碼1.1中的callback方法即為一個簡單的回調方法。readFile發起一個系統調用,隨后執行結束,當系統調用完成后,再通過回調函數獲得文件的內容。

1.2.2 同步/異步和阻塞/非阻塞

1.同步與異步

同步和異步描述的是進程/線程的調用方式。

同步調用指的是進程/線程發起調用后,一直等待調用返回后才繼續執行下一步操作,這并不代表CPU在這段時間內也會一直等待,操作系統多半會切換到另一個進程/線程上去,等到調用返回后再切換回原來的進程/線程。

異步就相反,發起調用后,進程/線程繼續向下執行,當調用返回后,通過某種手段來通知調用者。

注意:同步和異步中的“調用返回”,是指內核進程將數據復制到調用進程(Linux環境下)。

我們常常說JavaScript是一門異步的語言,但ECMAScript里并沒有關于異步的規范,JavaScript的異步更多是依靠瀏覽器runtime內部其他線程來實現,并非JavaScript本身的功能,是瀏覽器提供的支持讓JavaScript看起來像是一個異步的語言。

2.阻塞與非阻塞

阻塞與非阻塞的概念是針對IO狀態而言的,關注程序在等待IO調用返回這段時間的狀態。

關于Node中的IO,這里依然借用官網的說法:

需要注意的是,Node也沒有使用asynchronous(異步)之類的詞匯,而是使用了non-blocking(非阻塞)這樣的描述。

阻塞/非阻塞和同步/異步完全是兩組概念,它們之間沒有任何的必然聯系。很多人認為,阻塞=同步,非阻塞=異步,這種觀念是不正確的。我們下面介紹的IO編程模型中,除了純粹的AIO之外,阻塞和非阻塞IO都是同步的。

在介紹IO編程模型之前,先回答兩個問題。

(1)什么是IO操作

輸入/輸出(I/O)是在內存和外部設備(如磁盤、終端和網絡)之間復制數據的過程。

在實踐中IO操作幾乎無處不在,因為大多數程序都要產生輸出結果才有意義(往往是輸出到磁盤或者屏幕),除非你只在內存中計算一個斐波那契數列而且不采取其他任何操作。

在Node中,IO特指Node程序在Libuv支持下與系統磁盤和網絡交互的過程。

(2)IO調用的結果怎么返回給調用的進程/線程

通過內核進程復制給調用進程,在Linux下,用戶進程沒辦法直接訪問內核空間,通常是內核調用copy_to_user方法來傳遞數據的,大致的流程就是IO的數據會先被內核空間讀取,然后內核將數據復制給用戶進程。還有一種零復制技術,大致是內核進程和用戶進程共享一塊內存地址,這避免了內存的復制,讀者可自行搜索相關內容。

3.IO編程模型

編程模型是指操作系統在處理IO時所采用的方式,這通常是為了解決IO速度比較慢的問題而誕生的。

一般來說,編程模型有以下幾種:

  • blocking I/O
  • non-blocking I/O
  • I/O multiplexing(select and poll)
  • signal driven I/O(SIGIO)
  • asynchronous I/O(the POSIX aio_functions)

上面的5種模型中,signal driven I/O模型不常用,我們主要討論其他4種,它們均特指Linux下的IO模型。

(1)阻塞IO(blocking I/O)

對于IO來說,通常可以分為兩個階段,準備數據和返回結果,阻塞型IO在進程發出一個系統調用請求之后,進程就一直等待上述兩個階段完成,等待拿到返回結果之后再重新運行。

(2)非阻塞IO(nonblocking I/O)

和上面的過程相似,不同之處是當進程發起一個調用后,如果數據還沒有就緒,就會馬上返回一個結果告訴進程現在還沒有就緒,和阻塞IO的區別是用戶進程會不斷查詢內核狀態。這個過程依舊是同步的。

(3)IO multiplexing/Event Driven

這種IO通常也被稱為事件驅動IO,同樣是以輪詢的方式來查詢內核的執行狀態,和非阻塞IO的區別是一個進程可能會管理多個IO請求,當某個IO調用有了結果之后,就返回對應的結果。

注意:select和poll都是IO復用的機制,另外Node使用epoll(改進后的poll),這里不再詳細介紹。

(4)Asynchronous I/O

異步IO的概念讀者應該很熟悉了,和前面的模型相比,當進程發出調用后,內核會立刻返回結果,進程會繼續做其他的事情,直到操作系統返回數據,給用戶進程發送一個信號。注意,異步IO并沒有涉及任何關于回調函數的概念,此外,這里的異步IO只存在于Linux系統下。

讀者可能會感到好奇,那么既然如此,為什么在官網上Node沒有標榜自己是異步IO,而是寫成非阻塞IO呢?

很簡單,因為非阻塞是實打實的,而Node中的“異步I/O”是依靠Libuv模擬出來的。我們會在下一節介紹。

用一句話來概括阻塞/非阻塞和同步/異步:

同步調用會造成調用進程的IO阻塞,異步調用不會造成調用進程的IO阻塞(引用自《Unix網絡編程》第三版6.2)。

1.2.3 單線程和多線程

其他語言(例如Java、C++等)都有多線程的語言特性,即開發者可以派生出多個線程來協同工作,在這種情況下,用戶的代碼是運行在多線程環境下的。

Node并沒有提供多線程的支持,這代表用戶編寫的代碼只能運行在當前線程中,用于運行代碼的事件循環也是單線程運行的。開發者無法在一個獨立進程中增加新的線程,但是可以派生出多個進程來達到并行完成工作的目的。

另一方面,Node的底層實現并非是單線程的,libuv會通過類似線程池的實現來模擬不同操作系統下的異步調用,這對開發者來說是不可見的。

Libuv中的多線程

開發者編寫的代碼運行在單線程環境中,這句話是沒錯的,但如果說整個Node都是依靠單線程運行的,那就不正確了,因為libuv中是有線程池的概念存在的。

Libuv是一個跨平臺的異步IO庫,它結合了UNIX下的libev和Windows下的IOCP的特性,最早由Node的作者開發,專門為Node提供多平臺下的異步IO支持。libuv本身是由C/C++語言實現的,Node中的非阻塞IO以及事件循環的底層機制,都是由libuv來實現的。

圖1-2講述了libuv的架構。

圖1-2

在Windows環境下,libuv直接使用Windows的IOCP(I/O Completion Port)來實現異步IO。在非Windows環境下,libuv使用多線程來模擬異步IO。

Node的異步調用是由libuv來支持的,以readFile為例,讀取文件的系統調用是由libuv來完成的,Node只負責調用libuv的接口,等數據返回后再執行對應的回調方法。

1.2.4 并行和并發

并行(Parallel)與并發(Concurrent)是兩個很常見的概念,兩者雖然中文譯名相似,但實質上差別很大。

在介紹下面的內容之前,有必要對這兩概念進行解釋,下面是一個簡單的比喻:

我們假設業務場景是排隊取火車票。

并發是假設有兩對人排隊,但只有一個取票機,為了公平起見,先由隊列一排頭的人上前取票,再由隊列二的一個人上前取票,兩個隊列都在向前移動。

并行同樣是兩隊人排隊取票,不同的是開放了兩個取票機,那么兩個隊列可以同時向前移動,速度是一個窗口的兩倍以上(避免了一個窗口在兩個隊列間切換)。

并發和并行對應了兩種需求,一個是希望計算機做更多的事(處理多個隊列),另一個是希望計算機能更快地完成任務(讓隊列以更快的速度向前移動)。

如圖1-3所示給出了并發和并行,以及順序程序(即串行程序)之間的關系與區別(該圖引用自《深入理解計算機系統》12.6的圖12-30)。

圖1-3

Node中的并發

單線程支持高并發,通常都是依靠異步+事件驅動(循環)來實現的,異步使得代碼在面臨多個請求時不會發生阻塞,事件循環提供了IO調用結束后調用回調函數的能力。

Java可以依靠多線程實現并發,Node本身不支持開發者書寫多線程的代碼,事件循環也是單線程運行的,但是通過異步和事件驅動能夠很好地實現并發。

有一句話非常出名:“除了你的代碼,一切都是并行的”,網絡上很多文章都會提到這句話。但稍微思考一下,就會發現這句話里的“并行”值得深究。我們稍后會繼續介紹,在那之前,先來看事件循環相關的內容。

主站蜘蛛池模板: 平谷区| 微山县| 永和县| 若尔盖县| 鹿泉市| 芷江| 兴仁县| 鄱阳县| 淮安市| 太原市| 什邡市| 随州市| 榆社县| 会泽县| 偏关县| 化州市| 邢台县| 翁源县| 玉门市| 招远市| 迁安市| 颍上县| 海南省| 门头沟区| 荥阳市| 连城县| 巴青县| 克拉玛依市| 监利县| 咸宁市| 宁远县| 三台县| 荣昌县| 石嘴山市| 杭锦后旗| 丹巴县| 息烽县| 宜州市| 苍梧县| 绵竹市| 龙江县|