1. 多任務(wù)的實(shí)現(xiàn)
多任務(wù)的實(shí)現(xiàn)只有三種方式:
- 多進(jìn)程
- 單進(jìn)程+多線程
- 多進(jìn)程+多線程
第三種過(guò)于復(fù)雜脉幢,實(shí)現(xiàn)很少歪沃。多進(jìn)程和多線程都會(huì)消耗 cpu,在線程和進(jìn)程之間切換也會(huì)消耗 cpu嫌松,但是進(jìn)程的開(kāi)銷(xiāo)更大沪曙,所以,多任務(wù)的實(shí)現(xiàn)一般都是單進(jìn)程下開(kāi)啟多個(gè)線程萎羔。
但是液走,起初大部分實(shí)現(xiàn) I/O 操作的庫(kù)都是阻塞型 I/O。因此贾陷,多線程下缘眶,某個(gè)線程進(jìn)行 I/O 操作,當(dāng)前線程就會(huì)被阻塞髓废。雖然這種方式不會(huì)影響其他線程巷懈,最直觀的就是多個(gè)用戶連接之間不會(huì)因?yàn)橐粋€(gè)連接進(jìn)行 I/O 操作就阻塞了其他用戶。但是這依然有一個(gè)問(wèn)題慌洪,阻塞期間 cpu 等待 I/O 操作的結(jié)束顶燕,浪費(fèi)了很多性能,導(dǎo)致這種方式實(shí)現(xiàn)的并發(fā)性能和瓶頸都不高冈爹。
2. 異步和非阻塞
阻塞和非阻塞是對(duì)于操作系統(tǒng)而言的涌攻,是操作系統(tǒng)執(zhí)行 I/O 操作的兩種方式。
內(nèi)核實(shí)現(xiàn)非阻塞 I/O 的幾種方法:
read 輪詢模式
cpu 一直在重復(fù)執(zhí)行 read 操作频伤,直到 I/O 操作完成恳谎;select 輪詢
輪詢檢查文件描述符中的狀態(tài),狀態(tài)未讀取完成了再調(diào)用 read 方法憋肖;poll
解決了select只能檢查1024個(gè)文件的限制因痛;epoll
收到 I/O 事件的通知之后才會(huì)執(zhí)行相關(guān)操作,否則處于休眠狀態(tài)瞬哼;
異步和同步是結(jié)果婚肆,基于內(nèi)核實(shí)現(xiàn)非阻塞 I/O 的原理,單線程模式下的非阻塞 I/O 的結(jié)果仍然是同步坐慰!
非阻塞 I/O 方式较性,雖然一定程度上減少了對(duì) cpu 的損耗用僧,但是其本質(zhì)還是需要等待 I/O 操作的完成,只是等待期間赞咙,cpu 可能處于休眠责循,可能處于輪詢,這個(gè)操作性能上可能比阻塞 I/O 更好攀操,但是結(jié)果仍然是同步院仿;
需要特別注意的是:
非阻塞 I/O 和阻塞 I/O 區(qū)別并不大!K俸汀歹垫!
所以,真正意義上的異步是通過(guò)多線程 + 事件響應(yīng)實(shí)現(xiàn)的颠放。Node.js 中的 libuv 就是封裝了 Linux 和 Window 兩種操作系統(tǒng)下異步 API 的實(shí)現(xiàn)排惨。
再次提醒,異步 I/O 和非阻塞 I/O 是不同的概念碰凶。異步 I/O 是結(jié)果暮芭,非阻塞 I/O 是操作系統(tǒng)執(zhí)行 I/O 的一種形式,且非阻塞 I/O 意義并不大欲低。
3. I/O的概念
IO 在計(jì)算機(jī)中指 Input/Output辕宏,也就是輸入和輸出。由于程序和運(yùn)行時(shí)數(shù)據(jù)是在內(nèi)存中駐留砾莱,由CPU這個(gè)超快的計(jì)算核心來(lái)執(zhí)行瑞筐,涉及到數(shù)據(jù)交換的地方,通常是磁盤(pán)恤磷、網(wǎng)絡(luò)等面哼,就需要 I/O 接口。實(shí)現(xiàn)了 I/O 接口和功能的設(shè)備叫做 I/O 設(shè)備扫步,比如磁盤(pán)、網(wǎng)卡等等匈子。
代碼的運(yùn)行河胎、計(jì)算、線程之間的切換等都是 cpu 來(lái)執(zhí)行虎敦。cpu 執(zhí)行的速度遠(yuǎn)遠(yuǎn)高于 I/O 設(shè)備的處理速度游岳,內(nèi)存的存取數(shù)據(jù)的速度也遠(yuǎn)遠(yuǎn)超出 I/O 設(shè)備存取數(shù)據(jù)的速度。
在同步 I/O 的框架中其徙,一個(gè)線程中如果開(kāi)始了 I/O 操作胚迫,那么這個(gè)線程中其他的操作都需要等待 I/O 操作的結(jié)束才能繼續(xù)進(jìn)行,也就是超高速的 cpu 等待龜速的 I/O 操作唾那。因此访锻,同步 I/O 的框架中存在的問(wèn)題就是性能的瓶頸就是 I/O 操作的速度。
另外,I/O 的同步和異步并不是物理屬性期犬,也就是說(shuō)和設(shè)備無(wú)關(guān)河哑,不是說(shuō) I/O 設(shè)備就只支持同步 I/O 。I/O 的同步和異步與否取決于功能的實(shí)現(xiàn)龟虎,也就是 API 背后使用的是何種實(shí)現(xiàn)方式璃谨。
Ryan Dahl 就提出,大部分人不設(shè)計(jì)一種更簡(jiǎn)單有效的程序的原因是因?yàn)樗麄兪褂玫搅送?I/O 的庫(kù)鲤妥。因?yàn)橥?I/O 只需要等待即可佳吞,而異步 I/O 涉及到事件、輪詢棉安、消息容达、通知等一系列開(kāi)發(fā)任務(wù)。說(shuō)白了就是懶??~~所以垂券,Ryan Dahl 就基于 Javascript 花盐,使用異步 I/O 的方式實(shí)現(xiàn)了許多支持服務(wù)器開(kāi)發(fā)的庫(kù)和接口。
PS:雖然現(xiàn)在的程序員使用異步編程特別普遍菇爪,甚至如果你不使用異步編程算芯,很可能會(huì)因?yàn)闈撛诘男阅軉?wèn)題被人嘲諷。但是在若干年前凳宙,當(dāng)時(shí)流行的 PHP 就是完全的阻塞編程熙揍,壓根不提供異步和多線程的 Api。而當(dāng)時(shí)的很多高級(jí)語(yǔ)言雖然提供異步的支持氏涩,但是程序員普遍更喜歡使用同步編程届囚。在眾多的高級(jí)語(yǔ)言中,將異步作為主要編程方式和設(shè)計(jì)理念的是尖,Node是首個(gè)意系。所以,了解了當(dāng)時(shí)的環(huán)境就能更好的了解 Node.js 誕生的意義和目的了饺汹。
4. 解決的問(wèn)題
源起:基于(阻塞性I/O +多線程)原理的多任務(wù)服務(wù)器存在的問(wèn)題
當(dāng)遇到并發(fā)問(wèn)題時(shí)蛔添,比如多個(gè)客戶端向服務(wù)器發(fā)起了連接請(qǐng)求。其他語(yǔ)言比如 Java 等都是為一個(gè)新的客戶連接創(chuàng)建一個(gè)線程兜辞,一個(gè)線程的開(kāi)銷(xiāo)大概是2M迎瞧,所以一個(gè)內(nèi)存為 8GB 的服務(wù)器大概支持的同時(shí)連接數(shù)為4000。因此更大的并發(fā)勢(shì)必造成更大的硬件成本逸吵。不僅如此凶硅,多服務(wù)器存在時(shí),一個(gè)請(qǐng)求可能被不同的服務(wù)器處理扫皱,所以所有服務(wù)器之間必須共享資源足绅,由此會(huì)增加架構(gòu)的復(fù)雜性捷绑。
也就是說(shuō),問(wèn)題的核心是:
多線程原理的多任務(wù)系統(tǒng)隨著并發(fā)數(shù)的提升會(huì)帶來(lái)硬件成本和架構(gòu)復(fù)雜性的提升编检。
5. 如何解決問(wèn)題
V8 javascript 引擎本身性能就比較高胎食;
V8 javascript 引擎由 C++ 編寫(xiě),該引擎具備可移植性允懂,Node.js 就將它用在了服務(wù)器開(kāi)發(fā)上厕怜;非阻塞I/O;
javascript 本身是單線程了蕾总,V8 javascript 引擎也是如此粥航,所以 Node.js 為 javascript 提供了非阻塞性 I/O 的特性。另外生百,javascript 在瀏覽器中應(yīng)用時(shí)递雀,Web 為其整合提供了諸如 DOM、BOM 等很多瀏覽器 Api蚀浆。 Node.js 也一樣缀程,,除了 ECMAScript 之外市俊,Node.js 提供了很多服務(wù)器開(kāi)發(fā)相關(guān)的 Api杨凑;事件響應(yīng)機(jī)制
事件響應(yīng)說(shuō)白了就是 event loop,在 Node.js 剛問(wèn)世時(shí)摆昧,這種思想還很少撩满,但是如今卻是非常的常見(jiàn),比如 iOS 中的 runtime绅你、runloop伺帘。
綜上,Node.js 使用上述的三種手段來(lái)解決了面臨的問(wèn)題忌锯,但是最關(guān)鍵的仍然是事件響應(yīng)機(jī)制和線程池的管理伪嫁,下面將會(huì)介紹真正的異步 I/O 的實(shí)現(xiàn)原理。
6. 真正的異步 I/O
Node.js 中實(shí)現(xiàn)異步 I/O 的框架如下:
Node.js 的本質(zhì)也是開(kāi)啟多線程汉规,至于執(zhí)行 I/O 操作非阻塞還是阻塞礼殊,其實(shí)并不重要,估計(jì)大部分仍然是阻塞针史。但是 java 也是多線程,那么這兩者有什么區(qū)別呢碟狞?為什么node.js 的并發(fā)數(shù)可以比 java 高那么多啄枕?
Java一個(gè)用戶一個(gè)線程,但是 Node.js 中只有需要進(jìn)行阻塞操作時(shí)族沃,比如 I/O 操作频祝、網(wǎng)絡(luò)請(qǐng)求操作時(shí)泌参,這個(gè)時(shí)候才會(huì)開(kāi)啟線程。也就是說(shuō)常空,因?yàn)橹挥性谛枰枞麜r(shí)才會(huì)開(kāi)啟線程沽一,所以相同的用戶連接數(shù),Node.js 中線程的開(kāi)啟時(shí)間會(huì)相對(duì)于 Java 中的少漓糙,那么最終就會(huì)造成最大并發(fā)數(shù)的區(qū)別铣缠。
其原理如下:
I/O 操作無(wú)論阻塞還是非阻塞,其實(shí)區(qū)別并不大昆禽。所以說(shuō)蝗蛙,node.js 和 java 最大的不同就是:
- java 是為每個(gè)用戶分配一個(gè)進(jìn)程,進(jìn)程中執(zhí)行 I/O 操作時(shí)醉鳖,就會(huì)阻塞捡硅,此時(shí) cpu 就在等待?應(yīng)該是在等待盗棵,如果不等待壮韭,那么就必須有通知等機(jī)制來(lái)完成通知和響應(yīng)。
- Node.js 是所有的用戶連接都在一個(gè)線程中纹因,當(dāng)需要 I/O 操作時(shí)喷屋,才從線程池中分配線程去進(jìn)行 I/O 操作,此時(shí)主線程繼續(xù)執(zhí)行后面的操作辐怕,I/O 線程會(huì)阻塞逼蒙。關(guān)鍵就在于,因?yàn)橛辛耸录氖琛⑼ㄖ润w系的支持是牢,此時(shí)的阻塞不需要 cpu 的等待,所以陕截,這里才是性能提高的關(guān)鍵驳棱。
另外,事件循環(huán)和通知機(jī)制就真的不再詳細(xì)解釋了农曲,附圖一張:
整個(gè)異步流程如下:
注:本章主要參考《深入淺出Node.js》
7. Node.js的局限性
Node.js 之所以廣泛用于 Web 類(lèi)的應(yīng)用中社搅,是因?yàn)檫@類(lèi)應(yīng)用在,服務(wù)器的主要任務(wù)是 I/O 密集型而不是計(jì)算密集型乳规。
I/O 密集型而不是計(jì)算密集型就不詳細(xì)介紹了形葬,可以參考之前的文章,一言以蔽之:
- I/O 密集型服務(wù)器的主要任務(wù)是資源的存和取暮的,單個(gè)任務(wù)對(duì) cpu 的消耗不大笙以;
- 計(jì)算密集型服務(wù)器的主要任務(wù)是執(zhí)行代碼中的運(yùn)算,單個(gè)任務(wù)對(duì) cpu 消耗大冻辩;
所以猖腕,Node.js 的服務(wù)器如果想大并發(fā)拆祈,那就不能有非常復(fù)雜的處理。
常見(jiàn)場(chǎng)景:
- IM 系統(tǒng)
在線人數(shù)很多倘感,但是在服務(wù)端的處理上放坏,基本只需要 get 和 insert 數(shù)據(jù); - 電商系統(tǒng)
同時(shí)購(gòu)物的人數(shù)很多老玛,但是服務(wù)器上
8. 總結(jié):
一般的服務(wù)器都是阻塞性 I/O +多線程來(lái)實(shí)現(xiàn)多任務(wù)體系以支持高并發(fā)淤年。而 Node.js 采用的是 Javascript ,其本身就是單線程逻炊,支持各種異步操作互亮,再配上 Node.js 自身實(shí)現(xiàn)的異步 I/O 的網(wǎng)絡(luò)處理的各種庫(kù),最終完美實(shí)現(xiàn)了多線程+事件機(jī)制的高性能高并發(fā)服務(wù)器余素。
再次提醒:Node.js 異步的實(shí)現(xiàn)的關(guān)鍵不在于多線程豹休,而在于將 I/O 操作進(jìn)行多線程處理以達(dá)到不阻塞執(zhí)行代碼的線程的目的。Java 雖然也是多線程桨吊,但是代碼執(zhí)行和 I/O 操作總是處在一個(gè)線程威根。