現(xiàn)代操作系統(tǒng)比如,Linux,Windows等,都是支持“多任務”的操作系統(tǒng)。所謂多任務,指的就是操作系統(tǒng)可以同時運行多個任務。也就是在同一臺電腦上,可以同時上網(wǎng)、聽歌、使用Word,在過去單核的CPU上都已經(jīng)可以支持多任務,實現(xiàn)的方式是操作系統(tǒng)讓各個任務輪流交替執(zhí)行。,比如任務1執(zhí)行0.01秒,切換到任務2,任務2執(zhí)行0.01秒,再切換到任務3,執(zhí)行0.01秒,因為CPU執(zhí)行速度非常塊,我們感覺到所有任務都是并發(fā)處理。
到了多核CPU時代,由于任務數(shù)量還是遠遠多過CPU的核心數(shù)量,所以操作系統(tǒng)也會自動把多任務輪流調(diào)度到每個核心上執(zhí)行。
何為“進程(Process)”,對操作系統(tǒng)來說一個任務就是一個進程,比如打開了迅雷,而進程有可能不止同時干一件事,比如迅雷可以同時下載多個鏈接,瀏覽器可以開多個頁面等。把進程內(nèi)的這些“子任務”稱為線程(Thread)。
由于每個進程至少要干一件事,所以,一個進程至少有一個線程,多線程的執(zhí)行方式和多進程是一樣的,也是由操作系統(tǒng)在多個線程之間快速切換。
所以現(xiàn)在如果要執(zhí)行多個任務有兩種解決方案。
- 啟動多個進程,每個進程只有一個線程,這樣多個進程可以一塊執(zhí)行多個任務
- 啟動一個進程,然后每個進程再啟動多個線程,多個線程就可以一塊執(zhí)行任務。
當然也可以多個進程,再開多個線程,這樣模型太復雜,很少使用。
總結一下,多任務有三種方式
- 多進程模式;
- 多線程模式;
- 多進程+多線程模式。
引用知乎上的一個比喻:
1.單進程單線程:一個人在一個桌子上吃菜。
2.單進程多線程:多個人在同一個桌子上一起吃菜。所以多個人同時吃一道菜的時候容易發(fā)生爭搶,也就是說資源共享就會發(fā)生沖突爭搶
3.多進程單線程:多個人每個人在自己的桌子上吃菜。
對于Windows來說,加一張桌子開銷很大,所以 Windows 鼓勵大家在一個桌子上吃菜,所以需要面對線程資源爭搶與同步的問題。
對Linux而言,開一張新桌子開銷很小,所以可以盡可能多開新桌子,但是在不同桌子上說話不方便,所以需要研究進程間的通信。
那么什么時候適合使用多任務呢?需要執(zhí)行耗時操作的時候。比如發(fā)起一條網(wǎng)絡請求,因為網(wǎng)速的原因,服務器不會立刻響應我們 ,如果不開多任務,則主任務會阻塞住,從而影響體驗。
現(xiàn)在的服務器的CPU一般會有多個核心。
- 當每個 CPU 核心運行一個進程的時候,由于每個進程的資源都獨立,所以 CPU 核心之間切換的時候無需考慮上下文。
- 當每個 CPU 核心運行一個線程的時候,由于每個線程需要共享資源,所以這些資源必須從 CPU 的一個核心被復制到另外一個核心,才能繼續(xù)運算,這占用了額外的開銷。
換句話說,在 CPU 為多核的情況下,多線程在性能上未必趕得上多進程
總結:

下面詳細的介紹一下進程和線程
進程
無操作系統(tǒng)的時候,程序只能順序執(zhí)行,當引入了多任務操作系統(tǒng)以后,如果程序之間沒有依賴關系 ,則可以并發(fā)執(zhí)行。
我們可以將程序的一次執(zhí)行稱為一個進程,既然進程是動態(tài)的,所以它從創(chuàng)建到結束有一個完整的生命周期,而且CPU等資源是非常有限的,不是所有進程就可以立即占用CPU資源的,所以我們需要一個數(shù)據(jù)結構來記錄和描述進程處于什么樣的階段,這樣操作系統(tǒng)才可以全局調(diào)度。
為了使進程可以并發(fā)執(zhí)行,為進程引入了進程控制塊,即進程控制塊(Processing Control Block)。這個數(shù)據(jù)結構記錄了進程的相關信息比如要運行的指令地址,進程的狀態(tài),CPU暫存器等。也就是說操作系統(tǒng)其實是根據(jù)PCB的信息來對并發(fā)進程進行控制和管理的。如果沒有PCB的話,系統(tǒng)無法感知到進程,所以PCB是進程存在的唯一標志。
那么PCB主要包含什么呢?
- 進程標識:為進程分配一個數(shù)據(jù)ID,而且要唯一,相當于有了唯一的地址,操作系統(tǒng)才能找到它。
- 進程狀態(tài)信息:也就是進程現(xiàn)在處于什么階段,優(yōu)先級如何。
- 進程控制信息:進程對應程序的地址、消息隊列指針等。
- 存放CPU寄存器中的暫存信息
整個計算機系統(tǒng)中存在多個PCB,為了方便查找,將具有相同狀態(tài)的PCB通過鏈表的形式組成一個隊列 。而且因為PCB會經(jīng)常用到,所以常駐內(nèi)存
進程的狀態(tài)
進程具有三種基本的狀態(tài)
- 就緒:除了還沒有獲得CPU,其他的都已經(jīng)準備妥當了。
- 活動:占有CPU的狀態(tài)
-
阻塞:比如請求IO,還沒有獲得磁盤返回的ACK信號,則會阻塞等待。
image.png
- 就緒狀態(tài)的進程已經(jīng)準備妥當,只差分配CPU資源,放入就緒隊列,一旦獲得CPU,則進行執(zhí)行狀態(tài),當時間片用完則又回到就緒狀態(tài)
- 運行狀態(tài)的程序,如果要執(zhí)行IO請求,則會自己把自己阻塞掉,插入阻塞隊列。IO完成以后,又回到就緒狀態(tài),插入就緒隊列,等待再次調(diào)度。
多進程
Unix/Linux操作系統(tǒng)提供了一個fork()系統(tǒng)調(diào)用,它非常特殊。普通的函數(shù)調(diào)用,調(diào)用一次,返回一次,但是fork()調(diào)用一次,返回兩次,因為操作系統(tǒng)自動把當前進程(稱為父進程)復制了一份(稱為子進程),然后,分別在父進程和子進程內(nèi)返回。
子進程永遠返回0,而父進程返回子進程的ID。這樣做的理由是,一個父進程可以fork出很多子進程,所以,父進程要記下每個子進程的ID,而子進程可以很容易拿到父進程的ID。
有了fork調(diào)用,一個進程在接到新任務時就可以復制出一個子進程來處理新任務,常見的Apache服務器就是由父進程監(jiān)聽端口,每當有新的http請求時,就fork出子進程來處理新的http請求。
進程通信
進程通信指的是進程之間交互信息或者數(shù)據(jù)。Process之間肯定是需要通信的,操作系統(tǒng)提供了很多機制來實現(xiàn)進程間的通信,比如
- 通過消息Queue: 消息隊列是內(nèi)容為消息的鏈表
- 通過管道Pipe:管道是一種半雙工的通信方式,數(shù)據(jù)只能單向流動
- 通過共享內(nèi)存:也就是所有進程可以在一段共有的內(nèi)存上訪問數(shù)據(jù)。

線程
進程是運行著的程序,已經(jīng)可以實現(xiàn)多個進程之間的并發(fā)了,但是若進程是資源的持有者,每次進程的切換都意味著資源的獲得和釋放,對性能消耗太大了。所以引入“更輕量的進程”也就是線程。
也就是說進程只作為資源的持有者,然后開若干個線程進行并行處理,同一個進程中的線程共享此進程的資源,只有從一個進程中的線程切換到另一個進程中才會引起進程的調(diào)度。
總之,進程是資源的擁有者,而線程變成了獨立調(diào)度的基本單位,線程本身不擁有資源,只是共享其所在的進程的資源。
進程關注的是內(nèi)存的占有,每個進程所能訪問的內(nèi)存都是圈好的,而線程作為進程的一部分,扮演的角色就是怎么利用中CPU去運行代碼,所以關注的是中央處理器的運行,而不是內(nèi)存等資源的管理。
線程的實現(xiàn)方式
線程有兩種實現(xiàn)方式
- 內(nèi)核支持線程:線程的創(chuàng)建、撤銷、切換都是依賴于內(nèi)核空間。好處是SMP(對稱處理器)可以調(diào)度同一進程中多個線程并行執(zhí)行。缺點是線程切換的開銷比較大,因為線程調(diào)度和管理在內(nèi)核(可以看做高權限狀態(tài)),但是實際上大多數(shù)線程運行在用戶態(tài)(可以看做低權限狀態(tài)),所以切換線程需要系統(tǒng)調(diào)用,進入內(nèi)核態(tài),開銷自然就大了。
- 用戶級線程:指的是線程創(chuàng)建、同步、通信等不需要使用系統(tǒng)調(diào)用來實現(xiàn),都在用戶態(tài)完成。好處是與操作系統(tǒng)平臺無關,屬于用戶程序的一部分。比如數(shù)據(jù)庫大多都是這樣。但是這樣的話,就不能利用多處理器的優(yōu)點了,一個進程只能分配給一個CPU,而此進程中只有一個線程可執(zhí)行。
對于用戶級線程的實現(xiàn)需要通過中間系統(tǒng)比如運行時系統(tǒng)和內(nèi)核控制線程。
所謂運行時系統(tǒng)指的是管理和控制線程的函數(shù)集合,
第二種方法是通過輕型進程作為中間系統(tǒng),這些輕型進程可以通過系統(tǒng)調(diào)用來獲得內(nèi)核服務,若干個輕型進程形成線程池,而用戶級線程運行的時候,只要從池子里面取一個輕型進程連接上,就可以獲得如內(nèi)核進程一樣的服務了。

多線程
多任務可以由多進程完成,也可以由一個進程內(nèi)的多線程完成。由于任何進程默認就會啟動一個線程,我們把該線程稱為主線程,主線程又可以啟動新的線程
多線程和多進程最大的不同在于,多進程中,同一個數(shù)據(jù),各自有一份拷貝存在于每個進程中,互不影響,而多線程中,所有變量都由所有線程共享,所以,任何一個數(shù)據(jù)都可以被任何一個線程修改,因此,線程之間共享數(shù)據(jù)最大的危險在于多個線程同時改一個變量,把內(nèi)容給改亂了。所以仍然可以給臨界區(qū)代碼加一把鎖,因此其他線程不能同時執(zhí)行此代碼,只能等待,直到鎖被釋放后,獲得該鎖以后才能改。由于鎖只有一個,無論多少線程,同一時刻最多只有一個線程持有該鎖,所以,不會造成修改的沖突。獲得鎖的線程用完后一定要釋放鎖,否則那些苦苦等待鎖的線程將永遠等待下去,成為死線程
鎖的好處就是確保了某段關鍵代碼只能由一個線程從頭到尾完整地執(zhí)行,壞處是
- 阻止了多線程并發(fā)執(zhí)行,包含鎖的某段代碼實際上只能以單線程模式執(zhí)行,效率就大大地下降了。
- 由于可以存在多個鎖,不同的線程持有不同的鎖,并試圖獲取對方持有的鎖時,可能會造成死鎖,導致多個線程全部掛起,既不能執(zhí)行,也無法結束,只能靠操作系統(tǒng)強制終止。
image.png
進程VS線程
多進程和多線程是多任務最主要的兩種方式。要實現(xiàn)多任務,通常會使用計Master-Worker模式,Master負責分配任務,Worker負責執(zhí)行任務,因此,多任務環(huán)境下,通常是一個Master,多個Worker。
多進程模式:
- 優(yōu)點:穩(wěn)定性高,因為一個子進程崩潰了,不會影響主進程和其他子進程。(當然主進程掛了所有進程就全掛了,但是Master進程只負責分配任務,掛掉的概率低)。Apache最早就是采用多進程模式。
- 缺點:創(chuàng)建進程的代價大,Unix/Linux系統(tǒng)的進程創(chuàng)建開銷比較小,但是在Windows下創(chuàng)建進程開銷巨大。而且,操作系統(tǒng)能同時運行的進程數(shù)也是有限的,在內(nèi)存和CPU的限制下,如果有幾千個進程同時運行,操作系統(tǒng)連調(diào)度都會成問題。
對多線程模式而言,多線程模式通常比多進程快一點,但是也快不到哪去。而且,多線程模式最大的缺點就是任何一個線程掛掉都可能直接造成整個進程崩潰,因為所有線程共享進程的內(nèi)存。在Windows下,多線程的效率比多進程要高,所以微軟的IIS服務器默認采用多線程模式。由于多線程存在穩(wěn)定性的問題,IIS的穩(wěn)定性就不如Apache。
其實不管是多線程還是多進程,一旦數(shù)量上去了,效率都不會太高。如果做作業(yè),先花1小時做語文作業(yè),做完了,再花1小時做數(shù)學作業(yè),這樣,依次全部做完,一共花5小時,這種方式稱為單任務模型,或者批處理任務模型。
然后到了多任務模型,做1分鐘語文,再切換到數(shù)學作業(yè),做1分鐘,再切換到英語,以此類推,只要切換速度足夠快,這種方式就和單核CPU執(zhí)行多任務是一樣的了,看上去就正在同時寫5科作業(yè)。
但是注意,切換是要付出代價的。從語文切到數(shù)學,要先收拾桌子上的語文書(保存現(xiàn)場),然后,打開數(shù)學課本(這叫準備新環(huán)境)。
操作系統(tǒng)在切換也一樣,要先保存當前執(zhí)行的現(xiàn)場環(huán)境(CPU寄存器狀態(tài)、內(nèi)存頁等),然后,把新任務的執(zhí)行環(huán)境準備好(恢復上次的寄存器狀態(tài),切換內(nèi)存頁等),才能開始執(zhí)行。如果有幾千個任務同時進行,操作系統(tǒng)可能就主要忙著切換任務,然后硬盤狂響,點窗口無反應,系統(tǒng)處于假死狀態(tài)。

異步IO
可以把任務分為計算密集型和IO密集型。
- 計算密集型:需要進行大量的計算才能完成,比如高清視頻解碼,此時對CPU的利用率應該是越高越好,不應該讓CPU浪費在任務的切換上,所以計算密集型任務同時進行的數(shù)量應當?shù)扔贑PU的核心數(shù)。
- IO密集型:涉及到網(wǎng)絡、磁盤IO的任務都是IO密集型任務,特點是CPU消耗很少,任務的大部分時間都在等待IO操作完成,常見的大部分任務都是IO密集型任務,比如Web應用。對于IO密集型任務,最合適的語言就是開發(fā)效率最高(代碼量最少)的語言,腳本語言是首選,C語言最差。
那么因為CPU和IO的速度有天壤之別,所以如果一個任務在執(zhí)行的過程中大部分時間都在等待IO操作,最好引入多進程模型或者多線程模型來支持多任務并發(fā)執(zhí)行。
為多個用戶服務,每個用戶都會分配一個線程,如果遇到IO導致線程被掛起,其他用戶的線程不受影響。
這樣雖然解決了并發(fā)問題,但是線程數(shù)量過多,CPU的時間就花在線程切換上了,所以引入了異步IO,如果充分利用的異步IO,就可以用單進程單線程模型來執(zhí)行多任務,這種全新的模型稱為事件驅(qū)動模型,Nginx就是支持異步IO的Web服務器。在多核CPU上,可以運行多個進程(數(shù)量與CPU核心數(shù)相同),充分利用多核CPU,這樣總的進程數(shù)量并不多,操作系統(tǒng)調(diào)度非常高效。
那么什么是異步IO呢?
所謂異步IO指的是當需要執(zhí)行一個耗時的IO操作時,它只發(fā)出IO指令,并不等待IO結果,然后就去進入下一輪消息處理。一段時間后,當IO返回結果時,再通知CPU直接獲取IO操作結果
異步IO模型需要一個消息循環(huán),在消息循環(huán)中,主線程不斷地重復“讀取消息-處理消息”這一過程,也就是說在“發(fā)出IO請求”到收到“IO完成”的這段時間里,異步IO模型的主線程并沒有休息,而是在消息循環(huán)中繼續(xù)處理其他消息,而不是像同步IO模型下,主線程只能掛起。這樣,一個線程就可以同時處理多個IO請求,并且沒有切換線程的操作。


