node.js 之所以能夠運行 JavaScript 程式碼,是因為底層依賴 google 在 chrome 中使用的 V8 引擎,它是一個跑得非常快的 JavaScript 引擎,速度快到 讓 JavaScript 的速度勝過諸多語言,不過仍然沒有突破 JavaScript 的限制,就是 單線程問題。
線程就是所謂的執行緒,用很簡單的比喻來說明線程的概念:
假設今天去某超市採購中秋烤肉的材料,這家超市釋出很好的折扣,讓大家願意拿出三倍券來該超市進行採購,不過生意實在太好,超出業主預期,業主當初預期聘請一名結帳人員就能夠負荷,結果現在大排長龍,結帳人員忙得不可開交,客人們也等到頭髮都白了...
從上面可以看出只有一人在處理結帳流程,當結帳的人變多了,自然會形成忙不過來的狀況。在電腦的世界裡也有相同的問題,假設電腦是單核心的 CPU,那每個資源請求就是只能 透過一個核心來處理 。
延續上面超市的例子,業主設置了多個收銀台,並且聘請了多名結帳人員來 分散結帳人流,降低了所有結帳人員的工作量,更能夠縮短客戶排隊的時間。可以發現業主透過增加設備與人員來解決結帳問題,在電腦的世界也是一樣,透過資源的擴充來彌補硬體資源上的不足,讓每個資源請求能夠 透過多個核心來處理。
看完上述的舉例之後應該會有部分人感到困惑:明明電腦是多核心處理器,為什麼說 JavaScript 只能單線程?這個道理很簡單,同樣用超市舉例來說明,假設今天這家超市本來就已經有 8 個收銀台,但業主就是只聘請一名結帳人員,那有再多收銀台也沒有意義,簡單來說就是 JavaScript 本身只支援使用單一線程來運行,CPU 再多核也無法改善這個問題。
不過說 node.js 只支援單線程又有點不太對,主要是因為 node.js 不是只靠 V8 引擎在運作,底層依賴的東西非常多,這時候就要搬出 node.js 的運作圖了:
node Application 就是我們平常寫的 node.js 應用程式,在這個層級的程式碼會透過 V8 引擎 執行,並經由 node.js API 去跟使用 C 語言撰寫的 libuv 進行互動,而 libuv 提供了 Event Loop 讓單線程的操作可以實現 異步不阻塞,這也是 node.js 可以執行得 像多線程 的原因。事實上大多數情況 node.js 是單線程在運作,但只要是跟 I/O 有關的操作,libuv 就會使用多線程的方法處理,等於說 node.js 並不完全是單線程。
這是一個很有趣的機制,再沿用超市的例子來說明:在只有一名結帳人員的情況下,已經很忙了,這時遇到了一名客人問說他要結帳的 X 牌洗髮精是否有一整箱的,他想要買整箱的量,此時的結帳人員透過對講機請倉庫人員協助處理,於是先請他到旁邊等待,並幫下一位客人結帳,等到整箱的洗髮精來了之後,結帳人員就繼續幫他結帳,這樣的處理方式可以避免浪費過多的時間在等待。而 Event Loop 就是在處理這件事,透過 Event Queue 儲存需要等待的異步行為並持續關注,等到該行為已經結束異步行為時,就會再把它繼續處理完,這樣的機制可以讓在後方的行為不受等待所影響,形成永不阻塞的通道,正是所謂異步不阻塞。
跟 Express 並沒有直接關係,但絕對有間接關係,因為 Express 就是 node.js 的 Web 框架,而線程這件事又會影響到整體系統的效能,所以我認為這是蠻值得提的內容,試想今天是一個大型系統並且只用單線程在運行,資源根本沒有用到最佳化,使用者也無法體驗到最佳的系統效能,當流量上來的時候,客服電話也會著上來,不只系統會負荷不了,客服人員也會接到手痠!
雖然 node.js 背後有 Event Loop 解決異步阻塞的問題,但終究是用單線程處理大多數的使用場景,當流量很大的時候仍然是個挑戰。為了解決這個問題,node.js 推出了 叢集(Cluster) 功能。
叢集可以實現多線程的功能,將程式分為 父程式(master) 與 子程式(worker),他們之間的程式碼是共享的,但記憶體空間並非共享的,簡單來說就是:跑一個 node.js 的應用只能單線程,那我跑多個不就可以多線程了嗎!透過 Cluster 可以使他們共享同一個 port,這樣用起來就跟多線程沒兩樣了!
叢集是很棒的功能,但我並不打算自己實作它,因為已經有造好的輪子可以使用,我們只需要了解這基本概念即可!
有的時候要開發一個東西,先去了解這些工具背後的基本運作模式事實上是有幫助的,在知道如何運作之後,要去實作較有深度的功能時,會比較知道為什麼需要這麼做,比如說:使用 Cluster 開啟多線程,就是為了解決 node.js 在大多數場景下只能以單線程來運行。我在最後有提到已經有造好的輪子可以實現 Cluster 所做的事情,這部分我就在下一篇進行說明吧!