99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

(五)初探Node.js的異步I/O實現(xiàn)

2018-02-24 16:10 更新

專欄的第五篇文章《Node.js的異步實現(xiàn)》。之前介紹了Node.js的事件機制,也許讀者對此尚會覺得意猶未盡,因為僅僅只是簡單的事件機制,并不能道盡Node.js的神奇。如果Node.js是一盤別開生面的磁帶,那么事件與異步分別是其A面和B面,它們共同組成了Node.js的別樣之處。本文將翻轉(zhuǎn)Node.js到B面,與你共同聆聽。

異步I/O

在操作系統(tǒng)中,程序運行的空間分為內(nèi)核空間和用戶空間。我們常常提起的異步I/O,其實質(zhì)是用戶空間中的程序不用依賴內(nèi)核空間中的I/O操作實際完成,即可進行后續(xù)任務(wù)。以下偽代碼模仿了一個從磁盤上獲取文件和一個從網(wǎng)絡(luò)中獲取文件的操作。異步I/O的效果就是getFileFromNet的調(diào)用不依賴于getFile調(diào)用的結(jié)束。

getFile("file_path");
getFileFromNet("url");

如果以上兩個任務(wù)的時間分別為m和n。采用同步方式的程序要完成這兩個任務(wù)的時間總花銷會是m + n。但是如果是采用異步方式的程序,在兩種I/O可以并行的狀況下(比如網(wǎng)絡(luò)I/O與文件I/O),時間開銷將會減小為max(m, n)。

異步I/O的必要性

有的語言為了設(shè)計得使應(yīng)用程序調(diào)用方便,將程序設(shè)計為同步I/O的模型。這意味著程序中的后續(xù)任務(wù)都需要等待I/O的完成。在等待I/O完成的過程中,程序無法充分利用CPU。為了充分利用CPU,和使I/O可以并行,目前有兩種方式可以達到目的:

  • 多線程單進程
    多線程的設(shè)計之處就是為了在共享的程序空間中,實現(xiàn)并行處理任務(wù),從而達到充分利用CPU的效果。多線程的缺點在于執(zhí)行時上下文交換的開銷較大,和狀態(tài)同步(鎖)的問題。同樣它也使得程序的編寫和調(diào)用復(fù)雜化。
  • 單線程多進程
    為了避免多線程造成的使用不便問題,有的語言選擇了單線程保持調(diào)用簡單化,采用啟動多進程的方式來達到充分利用CPU和提升總體的并行處理能力。 它的缺點在于業(yè)務(wù)邏輯復(fù)雜時(涉及多個I/O調(diào)用),因為業(yè)務(wù)邏輯不能分布到多個進程之間,事務(wù)處理時長要遠遠大于多線程模式。

前者在性能優(yōu)化上還有回旋的余地,后者的做法純粹是一種加三倍服務(wù)器的行為。?
而且現(xiàn)在的大型Web應(yīng)用中,單機的情形是十分稀少的,一個事務(wù)往往需要跨越網(wǎng)絡(luò)幾次才能完成最終處理。如果網(wǎng)絡(luò)速度不夠理想,m和n值都將會變大,這時同步I/O的語言模型將會露出其最脆弱的狀態(tài)。?
這種場景下的異步I/O將會體現(xiàn)其優(yōu)勢,max(m, n)的時間開銷可以有效地緩解m和n值增長帶來的性能問題。而當并行任務(wù)更多的時候,m + n + …與max(m, n, …)之間的孰優(yōu)孰劣更是一目了然。從這個公式中,可以了解到異步I/O在分布式環(huán)境中是多么重要,而Node.js天然地支持這種異步I/O,這是眾多云計算廠商對其青睞的根本原因。

操作系統(tǒng)對異步I/O的支持

我們聽到Node.js時,我們常常會聽到異步,非阻塞,回調(diào),事件這些詞語混合在一起。其中,異步與非阻塞聽起來似乎是同一回事。從實際效果的角度說,異步和非阻塞都達到了我們并行I/O的目的。但是從計算機內(nèi)核I/O而言,異步/同步和阻塞/非阻塞實際上時兩回事。

  • I/O的阻塞與非阻塞
    阻塞模式的I/O會造成應(yīng)用程序等待,直到I/O完成。同時操作系統(tǒng)也支持將I/O操作設(shè)置為非阻塞模式,這時應(yīng)用程序的調(diào)用將可能在沒有拿到真正數(shù)據(jù)時就立即返回了,為此應(yīng)用程序需要多次調(diào)用才能確認I/O操作完全完成。
  • I/O的同步與異步
    I/O的同步與異步出現(xiàn)在應(yīng)用程序中。如果做阻塞I/O調(diào)用,應(yīng)用程序等待調(diào)用的完成的過程就是一種同步狀況。相反,I/O為非阻塞模式時,應(yīng)用程序則是異步的。

異步I/O與輪詢技術(shù)

當進行非阻塞I/O調(diào)用時,要讀到完整的數(shù)據(jù),應(yīng)用程序需要進行多次輪詢,才能確保讀取數(shù)據(jù)完成,以進行下一步的操作。
輪詢技術(shù)的缺點在于應(yīng)用程序要主動調(diào)用,會造成占用較多CPU時間片,性能較為低下?,F(xiàn)存的輪詢技術(shù)有以下這些:

  • read
  • select
  • poll
  • epoll
  • pselect
  • kqueue

read是性能最低的一種,它通過重復(fù)調(diào)用來檢查I/O的狀態(tài)來完成完整數(shù)據(jù)讀取。select是一種改進方案,通過對文件描述符上的事件狀態(tài)來進行判斷。操作系統(tǒng)還提供了poll、epoll等多路復(fù)用技術(shù)來提高性能。
輪詢技術(shù)滿足了異步I/O確保獲取完整數(shù)據(jù)的保證。但是對于應(yīng)用程序而言,它仍然只能算時一種同步,因為應(yīng)用程序仍然需要主動去判斷I/O的狀態(tài),依舊花費了很多CPU時間來等待。

上一種方法重復(fù)調(diào)用read進行輪詢直到最終成功,用戶程序會占用較多CPU,性能較為低下。而實際上操作系統(tǒng)提供了select方法來代替這種重復(fù)read輪詢進行狀態(tài)判斷。select內(nèi)部通過檢查文件描述符上的事件狀態(tài)來進行判斷數(shù)據(jù)是否完全讀取。但是對于應(yīng)用程序而言它仍然只能算是一種同步,因為應(yīng)用程序仍然需要主動去判斷I/O的狀態(tài),依舊花費了很多CPU時間等待,select也是一種輪詢。

理想的異步I/O模型

理想的異步I/O應(yīng)該是應(yīng)用程序發(fā)起異步調(diào)用,而不需要進行輪詢,進而處理下一個任務(wù),只需在I/O完成后通過信號或是回調(diào)將數(shù)據(jù)傳遞給應(yīng)用程序即可。

幸運的是,在Linux下存在一種這種方式,它原生提供了一種異步非阻塞I/O方式(AIO)即是通過信號或回調(diào)來傳遞數(shù)據(jù)的。
不幸的是,只有Linux下有這么一種支持,而且還有缺陷(AIO僅支持內(nèi)核I/O中的O_DIRECT方式讀取,導(dǎo)致無法利用系統(tǒng)緩存。參見:http://forum.nginx.org/read.php?2,113524,113587#msg-113587
以上都是基于非阻塞I/O進行的設(shè)定。另一種理想的異步I/O是采用阻塞I/O,但加入多線程,將I/O操作分到多個線程上,利用線程之間的通信來模擬異步。Glibc的AIO便是這樣的典型http://www.ibm.com/developerworks/linux/library/l-async/。然而遺憾在于,它存在一些難以忍受的缺陷和bug??梢院唵蔚母攀鰹椋篖inux平臺下沒有完美的異步I/O支持。
所幸的是,libev的作者Marc Alexander Lehmann重新實現(xiàn)了一個異步I/O的庫:libeio。libeio實質(zhì)依然是采用線程池與阻塞I/O模擬出來的異步I/O。
那么在Windows平臺下的狀況如何呢?而實際上,Windows有一種獨有的內(nèi)核異步IO方案:IOCP。IOCP的思路是真正的異步I/O方案,調(diào)用異步方法,然后等待I/O完成通知。IOCP內(nèi)部依舊是通過線程實現(xiàn),不同在于這些線程由系統(tǒng)內(nèi)核接手管理。IOCP的異步模型與Node.js的異步調(diào)用模型已經(jīng)十分近似。
以上兩種方案則正是Node.js選擇的異步I/O方案。由于Windows平臺和*nix平臺的差異,Node.js提供了libuv來作為抽象封裝層,使得所有平臺兼容性的判斷都由這一層次來完成,保證上層的Node.js與下層的libeio/libev及IOCP之間各自獨立。Node.js在編譯期間會判斷平臺條件,選擇性編譯unix目錄或是win目錄下的源文件到目標程序中。

在JavaScript層面上調(diào)用的fs.open方法最終都透過node_file.cc調(diào)用到了libuv中的uv_fs_open方法,這里libuv作為封裝層,分別寫了兩個平臺下的代碼實現(xiàn),編譯之后,只會存在一種實現(xiàn)被調(diào)用。

請求對象

在uv_fs_open的調(diào)用過程中,Node.js創(chuàng)建了一個FSReqWrap請求對象。從JavaScript傳入的參數(shù)和當前方法都被封裝在這個請求對象中,其中回調(diào)函數(shù)則被設(shè)置在這個對象的oncomplete_sym屬性上。

req_wrap->object_->Set(oncomplete_sym, callback);

對象包裝完畢后,調(diào)用QueueUserWorkItem方法將這個FSReqWrap對象推入線程池中等待執(zhí)行。

QueueUserWorkItem(&uv_fs_thread_proc, req, WT_EXECUTELONGFUNCTION)

QueueUserWorkItem接受三個參數(shù),第一個是要執(zhí)行的方法,第二個是方法的上下文,第三個是執(zhí)行的標志。當線程池中有可用線程的時候調(diào)用uv_fs_thread_proc方法執(zhí)行。該方法會根據(jù)傳入的類型調(diào)用相應(yīng)的底層函數(shù),以uv_fs_open為例,實際會調(diào)用到fs__open方法。調(diào)用完畢之后,會將獲取的結(jié)果設(shè)置在req->result上。然后調(diào)用PostQueuedCompletionStatus通知我們的IOCP對象操作已經(jīng)完成。

PostQueuedCompletionStatus((loop)->iocp, 0, 0, &((req)->overlapped))

PostQueuedCompletionStatus方法的作用是向創(chuàng)建的IOCP上相關(guān)的線程通信,線程根據(jù)執(zhí)行狀況和傳入的參數(shù)判定退出。
至此,由JavaScript層面發(fā)起的異步調(diào)用第一階段就此結(jié)束。

事件循環(huán)

在調(diào)用uv_fs_open方法的過程中實際上應(yīng)用到了事件循環(huán)。以在Windows平臺下的實現(xiàn)中,啟動Node.js時,便創(chuàng)建了一個基于IOCP的事件循環(huán)loop,并一直處于執(zhí)行狀態(tài)。

uv_run(uv_default_loop());

每次循環(huán)中,它會調(diào)用IOCP相關(guān)的GetQueuedCompletionStatus方法檢查是否線程池中有執(zhí)行完的請求,如果存在,poll操作會將請求對象加入到loop的pending_reqs_tail屬性上。 另一邊這個循環(huán)也會不斷檢查loop對象上的pending_reqs_tail引用,如果有可用的請求對象,就取出請求對象的result屬性作為結(jié)果傳遞給oncomplete_sym執(zhí)行,以此達到調(diào)用JavaScript中傳入的回調(diào)函數(shù)的目的。 至此,整個異步I/O的流程完成結(jié)束。其流程如下:

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號