13.4 進程 vs. 線程
我們介紹了多進程和多線程,這是實現(xiàn)多任務(wù)最常用的兩種方式。現(xiàn)在,我們來討論一下這兩種方式的優(yōu)缺點。
首先,要實現(xiàn)多任務(wù),通常我們會設(shè)計Master-Worker模式,Master負責(zé)分配任務(wù),Worker負責(zé)執(zhí)行任務(wù),因此,多任務(wù)環(huán)境下,通常是一個Master,多個Worker。
如果用多進程實現(xiàn)Master-Worker,主進程就是Master,其他進程就是Worker。
如果用多線程實現(xiàn)Master-Worker,主線程就是Master,其他線程就是Worker。
多進程模式最大的優(yōu)點就是穩(wěn)定性高,因為一個子進程崩潰了,不會影響主進程和其他子進程。(當(dāng)然主進程掛了所有進程就全掛了,但是Master進程只負責(zé)分配任務(wù),掛掉的概率低)著名的Apache最早就是采用多進程模式。
多進程模式的缺點是創(chuàng)建進程的代價大,在Unix/Linux系統(tǒng)下,用fork
調(diào)用還行,在Windows下創(chuàng)建進程開銷巨大。另外,操作系統(tǒng)能同時運行的進程數(shù)也是有限的,在內(nèi)存和CPU的限制下,如果有幾千個進程同時運行,操作系統(tǒng)連調(diào)度都會成問題。
多線程模式通常比多進程快一點,但是也快不到哪去,而且,多線程模式致命的缺點就是任何一個線程掛掉都可能直接造成整個進程崩潰,因為所有線程共享進程的內(nèi)存。在Windows上,如果一個線程執(zhí)行的代碼出了問題,你經(jīng)常可以看到這樣的提示:“該程序執(zhí)行了非法操作,即將關(guān)閉”,其實往往是某個線程出了問題,但是操作系統(tǒng)會強制結(jié)束整個進程。
在Windows下,多線程的效率比多進程要高,所以微軟的IIS服務(wù)器默認采用多線程模式。由于多線程存在穩(wěn)定性的問題,IIS的穩(wěn)定性就不如Apache。為了緩解這個問題,IIS和Apache現(xiàn)在又有多進程+多線程的混合模式,真是把問題越搞越復(fù)雜。
線程切換
無論是多進程還是多線程,只要數(shù)量一多,效率肯定上不去,為什么呢?
我們打個比方,假設(shè)你不幸正在準備中考,每天晚上需要做語文、數(shù)學(xué)、英語、物理、化學(xué)這5科的作業(yè),每項作業(yè)耗時1小時。
如果你先花1小時做語文作業(yè),做完了,再花1小時做數(shù)學(xué)作業(yè),這樣,依次全部做完,一共花5小時,這種方式稱為單任務(wù)模型,或者批處理任務(wù)模型。
假設(shè)你打算切換到多任務(wù)模型,可以先做1分鐘語文,再切換到數(shù)學(xué)作業(yè),做1分鐘,再切換到英語,以此類推,只要切換速度足夠快,這種方式就和單核CPU執(zhí)行多任務(wù)是一樣的了,以幼兒園小朋友的眼光來看,你就正在同時寫5科作業(yè)。
但是,切換作業(yè)是有代價的,比如從語文切到數(shù)學(xué),要先收拾桌子上的語文書本、鋼筆(這叫保存現(xiàn)場),然后,打開數(shù)學(xué)課本、找出圓規(guī)直尺(這叫準備新環(huán)境),才能開始做數(shù)學(xué)作業(yè)。操作系統(tǒng)在切換進程或者線程時也是一樣的,它需要先保存當(dāng)前執(zhí)行的現(xiàn)場環(huán)境(CPU寄存器狀態(tài)、內(nèi)存頁等),然后,把新任務(wù)的執(zhí)行環(huán)境準備好(恢復(fù)上次的寄存器狀態(tài),切換內(nèi)存頁等),才能開始執(zhí)行。這個切換過程雖然很快,但是也需要耗費時間。如果有幾千個任務(wù)同時進行,操作系統(tǒng)可能就主要忙著切換任務(wù),根本沒有多少時間去執(zhí)行任務(wù)了,這種情況最常見的就是硬盤狂響,點窗口無反應(yīng),系統(tǒng)處于假死狀態(tài)。
所以,多任務(wù)一旦多到一個限度,就會消耗掉系統(tǒng)所有的資源,結(jié)果效率急劇下降,所有任務(wù)都做不好。
計算密集型 vs. IO密集型
是否采用多任務(wù)的第二個考慮是任務(wù)的類型。我們可以把任務(wù)分為計算密集型和IO密集型。
計算密集型任務(wù)的特點是要進行大量的計算,消耗CPU資源,比如計算圓周率、對視頻進行高清解碼等等,全靠CPU的運算能力。這種計算密集型任務(wù)雖然也可以用多任務(wù)完成,但是任務(wù)越多,花在任務(wù)切換的時間就越多,CPU執(zhí)行任務(wù)的效率就越低,所以,要最高效地利用CPU,計算密集型任務(wù)同時進行的數(shù)量應(yīng)當(dāng)?shù)扔贑PU的核心數(shù)。
計算密集型任務(wù)由于主要消耗CPU資源,因此,代碼運行效率至關(guān)重要。Python這樣的腳本語言運行效率很低,完全不適合計算密集型任務(wù)。對于計算密集型任務(wù),最好用C語言編寫。
第二種任務(wù)的類型是IO密集型,涉及到網(wǎng)絡(luò)、磁盤IO的任務(wù)都是IO密集型任務(wù),這類任務(wù)的特點是CPU消耗很少,任務(wù)的大部分時間都在等待IO操作完成(因為IO的速度遠遠低于CPU和內(nèi)存的速度)。對于IO密集型任務(wù),任務(wù)越多,CPU效率越高,但也有一個限度。常見的大部分任務(wù)都是IO密集型任務(wù),比如Web應(yīng)用。
IO密集型任務(wù)執(zhí)行期間,99%的時間都花在IO上,花在CPU上的時間很少,因此,用運行速度極快的C語言替換用Python這樣運行速度極低的腳本語言,完全無法提升運行效率。對于IO密集型任務(wù),最合適的語言就是開發(fā)效率最高(代碼量最少)的語言,腳本語言是首選,C語言最差。
異步IO
考慮到CPU和IO之間巨大的速度差異,一個任務(wù)在執(zhí)行的過程中大部分時間都在等待IO操作,單進程單線程模型會導(dǎo)致別的任務(wù)無法并行執(zhí)行,因此,我們才需要多進程模型或者多線程模型來支持多任務(wù)并發(fā)執(zhí)行。
現(xiàn)代操作系統(tǒng)對IO操作已經(jīng)做了巨大的改進,最大的特點就是支持異步IO。如果充分利用操作系統(tǒng)提供的異步IO支持,就可以用單進程單線程模型來執(zhí)行多任務(wù),這種全新的模型稱為事件驅(qū)動模型,Nginx就是支持異步IO的Web服務(wù)器,它在單核CPU上采用單進程模型就可以高效地支持多任務(wù)。在多核CPU上,可以運行多個進程(數(shù)量與CPU核心數(shù)相同),充分利用多核CPU。由于系統(tǒng)總的進程數(shù)量十分有限,因此操作系統(tǒng)調(diào)度非常高效。用異步IO編程模型來實現(xiàn)多任務(wù)是一個主要的趨勢。
對應(yīng)到Python語言,單線程的異步編程模型稱為協(xié)程,有了協(xié)程的支持,就可以基于事件驅(qū)動編寫高效的多任務(wù)程序。我們會在后面討論如何編寫協(xié)程。