關於node.js的誤會

昨天寫瞭篇博客,介紹瞭一下我對node.js的第一次親密接觸後的感受,以為node.js很小眾,出乎我意料很多人感興趣,並且對博客中的細節問題做瞭評論,最多的是圍繞node.js的異步與單線程展開的,當然還有很多關於node.js究竟是不是語言?不是的話又是什麼。。。之類的問題,其實剛接觸node.js,瞭解的並不是很深入,越是回復大傢問題,心裡越是沒底,決定認真研究一下,經人指點看瞭一下《Node.js開發指南》發現大部分問題都有瞭答案,權當一個讀書筆記把問題答案分享出來,希望可以幫到一些和我一樣才接觸node.js的小菜

 

 關於單線程一個由來已久的誤會

在上篇博客中提到我們使用node.js寫的JavaScript代碼是單線程運行的,讓很多同學很疑惑,單線程怎麼實現異步操作,單線程誰去響應事件。。。在html5 Web Workers中我也有提到過客戶端的JavaScript也是單線程運行的,大傢明顯沒有這麼大反應,還是普遍能接受的。可單線程的客戶端JavaScript也能響應DOM事件,還有大傢都很熟悉的ajax操作,回調函數也是異步的,既然客戶端JavaScript是單線程執行的,回調函數是誰調用的呢?答案很簡單,JavaScript的宿主環境——瀏覽器,也就是說雖然JavaScript是單線程執行的,但瀏覽器是多線程的,負責調度管理JavaScript代碼,讓它們在恰當的時機執行。

 

所以我們所說的node.js單線程,是指node.js並沒有給我們創建一個線程的能力,所有我們自己寫的代碼都是單線程執行的,在同一時間內,隻能執行我們寫的一句代碼。但宿主環境node.js並不是單線程的,它會維護一個執行隊列,循環檢測,調度JavaScript線程來執行。因此單線程執行和並發操作並不沖突。

 

阻塞與線程

什麼叫阻塞(block)?線程在執行中如果遇到I/O操作(磁盤讀寫、網絡通信等)通常需要耗費較長的時間,這時候操作系統會剝奪線程對CPU的控制權,使其暫停,並把資源讓給其它的工作線程,這種線程調度方式成為阻塞。當I/O操作完畢的時候操作系統將這個線程的阻塞狀態解除,恢復其對CPU的控制權,令其繼續執行,這種I/O模式就是同步I/O或成為阻塞I/O。

 

響應的異步I/O或非阻塞I/O則針對所有的I/O操作采取不阻塞的策略,當線程遇到I/O操作時不會以阻塞的方式等待I/O操作結束,而隻是將I/O請求發送給操作系統,繼續執行後續語句。當操作系統完成I/O操作時以事件的形式通知執行I/O操作的線程,線程會在特定時間處理這個事件。為瞭處理異步I/O必須有事件循環,不斷檢查有沒有未處理的事件,依次予以處理。

 

在阻塞模式下,一個線程隻能處理一個任務,要想提高吞吐量必須通過多線程。而在非阻塞模式下一個線程永遠在執行計算操作,這個線程所使用的CPU核心利用率永遠是100%,I/O以事件的方式通知。在阻塞模式下多線程往往能夠提高系統吞吐量,因為一個線程阻塞時還有其他線程在工作,多線程何以讓CPU資源不被阻塞的線程浪費。而在非阻塞模式下,線程不會被I/O阻塞,永遠在利用CPU。異步I/O減少瞭多線程中創建線程、分配內存、列入調度、切換線程、內存換頁、CPU緩存等方面的開銷。

 

事件循環機制

上面提到瞭幾次事件循環機制,那麼這個聽起來貌似很高端的東東究竟是什麼呢?所謂事件循環是指node.js會把所有的異步操作使用事件機制解決,有個線程在不斷地循環檢測事件隊列。node.js中所有的邏輯都是事件的回調函數,所以node.js始終在事件循環中,程序入口就是事件循環第一個事件的回調函數。事件的回調函數中可能會發出I/O請求或直接發射( emit)事件,執行完畢後返回事件循環。事件循環會檢查事件隊列中有沒有未處理的事件,直到程序結束。node.js的事件循環對開發者不可見,由libev庫實現,libev不斷檢查是否有活動的、可供檢測的事件監聽器,直到檢查不到時才退出事件循環,程序結束。

 

 

node.js是什麼?和JavaScript有什麼關系?

 關於node.js究竟是什麼,大傢的問題在於

 

node.js是不是一門語言?

node.js是不是一個JavaScript庫函數?

node.js是不是一個JavaScript框架?

 很遺憾,這三個問題的答案都是NO,看看官方對自己的描述

 

Node.js is a platform built on Chrome's JavaScript runtime for easily building fast, scalable network applications. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient, perfect for data-intensive real-time applications that run across distributed devices.

 

官方很明確地說node.js是一個platform,也就是一個做xxx的平臺。node.js是一個可以在服務器端運行JavaScript的平臺,其實這句話華也不準確,按照《JavaScript權威指南》《JavaScript高級程序設計》等書中的定義,JavaScript是由兩部分組成

 

core JavaScript

client JavaScript(DOM、BOM)

而隻有core JavaScript可以在node.js上運行,所以node.js借用瞭JavaScript的語法,但並不能用來處理瀏覽器對象(BOM)及文檔對象(DOM),所以node.js並不是設計為在服務器端運行解析html文檔的(當然有module可以做此事),所以“在服務器端運行的JavaScript”在一定程度上誤導瞭初學者。

 

同時node.js並不僅僅運行core JavaScript,node.js中還有文件系統、模塊包、操作系統API、網絡通信、二進制類型處理等core JavaScript不具備的功能。

 

node.js是在執行JavaScript語句嗎

從字面意思上是的,因為我們在使用node.js開發的時候寫的確實是JavaScript語句,而且node.js利用Google的V8 Javascript 引擎來解析JavaScript語句,但系統真正調用執行的代碼是用C++寫的,我們做的隻是用JavaScript語句來調用這些底層API,所以不用擔心其執行效率過低問題

 

異步I/O與事件驅動

 毫不誇張的說node.js最大的特定就是采用異步I/O和事件驅動架構,對於高並發解決方案傳統架構師多線程模型,為每個業務邏輯童工一個線程,通過系統線程切換來來彌補同步I/O調用的時間開銷。node.js使用的是單線程模型,對所有I/O都采用異步式的請求方式,避免頻繁的上下文切換,在node.js執行的時候維護著一個事件隊列,程序在執行時進入事件循環等待下一個事件到來,每個異步I/O請求完成後都會被推送到事件隊列中的等待執行。

 

對於一個簡單的數據庫訪問操作,傳統方式是這樣實現的

 

 res = db.query('SELECT * from some_table');

 res.output();

 

 

代碼執行到第一行的時候線程會阻塞,等待query返回結果,然後繼續處理。由於數據庫查詢、磁盤讀寫、網絡通信等原因阻塞時間會非常大(相對於CPU始終頻率)。對於高並發的訪問,一方面線程長期阻塞等待,另一方面為瞭應付新情求而不斷添加新線程,會浪費大量系統資源,同時線程的增加也會也會占用大量的CPU時間來處理內存上下文切換。看看node.js怎麼處理

 

db.query('SELECT * from some_table', function(res) { 

   res.output();

});

 

 

在代碼中熟悉Javascript的同學一眼就可以看明白query的第二個參數是一個回調函數,進程執行到db.query的時候不會等待結果返回,而是直接繼續執行下面的語句,直到進入事件循環。當數據庫執行結果返回的時候會將事件發送到事件隊列,等到線程進入事件循環後才會調用之前的回調函數。

 

node.js的異步機制是基於事件的,所有的I/O、網絡通信、數據庫查詢都以非阻塞的方式執行,返回結果由事件循環來處理。node.js在同一時刻隻會處理一個事件,完成後立即進入事件循環檢查後面事件。這樣CPU和內存在同一時間集中處理一件事,同時盡量讓耗時的I/O等操作並行執行。

發佈留言