以生活例子說明單執行緒與多執行緒
閱讀目錄
- 1. 程式設計的目標
- 2. 單執行緒多工無阻塞
- 3. 單執行緒多工IO阻塞
- 4. 單執行緒多工非同步IO
- 5. 單執行緒多工,有耗時計算
- 6. 多執行緒程式
- 7. 多CPU
- 8. 多執行緒與多程式
- 9. 總結
1. 程式設計的目標
在我看來單從程式的角度來看,一個好的程式的目標應該是效能與使用者體驗的平衡。當然一個程式是否能夠滿足使用者的需求暫且不談,這是業務層面的問題,我們僅僅討論程式本身。圍繞兩點來展開,效能與使用者體驗。
效能:高效能的程式應該可以等同於CPU的利用率,CPU的利用率越高(一直在工作,沒有閒下來的時候),程式的效能越高。
體驗:這裡的體驗不只是介面多麼漂亮,功能多麼順手,這裡的體驗指程式的響應速度,響應速度越快,使用者體驗越好。
下面我們就這兩點進行各種模型的討論。
2. 單執行緒多工無阻塞
以生活中食堂打飯的場景作為比喻,假設有這樣的場景,小A,小B,小C 在視窗依次排隊打飯。 假設視窗負責打飯的阿姨打一個菜需要耗時1秒。如果小A需要2個菜,小B需要3個菜,小C需要2個菜。如下:
阿姨(CPU):打一個菜需要1秒
小A:2個菜
小B:3個菜
小C:2個菜
那麼在這種模型下將所有服務做完阿姨需要耗時 2 + 3 + 2 = 7秒
阿姨 = CPU
小A,小B,小C = 任務(這裡是以任務為概念,表示需要做一些事情)
這種模型下CPU是滿負荷不間斷運轉的,沒有空閒,使用者體驗還不錯。這種程式中每個任務的耗時都比較小,是非常理想的狀態,一般情況下基本不太可能存在。
3. 單執行緒多工IO阻塞
將上面的場景稍微做改動:
阿姨:打一個菜需要1秒
小A:2個菜,但是忘記帶錢了,要找同學送過來,估計需要等5分鐘可以送到(可以理解為磁碟IO)
小B:3個菜
小C:2個菜
這種情況下小A這裡發生了阻塞,實際上小A這裡耗費了5分鐘也就是 300秒+ 2個菜的時間,也就是302秒,而CPU則空閒了300秒,實際上工作2秒。
所有服務做完花費 302 + 3 + 2 = 307秒 CPU實際工作7秒,等待300秒。 極大浪費了CPU的時鐘週期。 使用者體驗很差,因為小A阻塞的時候,後面的所有人都等著,而實際上此時CPU空閒。所以單執行緒中不要有阻塞出現。
4. 單執行緒多工非同步IO
還是上面的模型,加入一個角色:值日生小哥,他負責事先詢問每一個人是否帶錢了,如果帶錢了則允許打菜,否則把錢準備好了再說。
<1> 值日生小哥問小A準備好打菜了嗎,小A說忘帶錢了,值日生小哥說,你把錢準備好了再說,小A開始準備(需要300秒,從此刻開始記時)。
<2> 值日生小哥問小B準備好打菜了嗎,小B說可以了,阿姨服務小B,耗時3秒
<3> 值日生小哥問小C準備好打菜了嗎,小C說可以了,阿姨服務小C,耗時2秒
<4> 值日生小哥問小A準備好了沒有,小A說還要等一會,阿姨由於沒有人過來服務,處於空閒狀態
<5> 300秒之後,小A準備好了,阿姨服務小A,耗時2秒
整個過程做完耗時 300 + 2 = 302秒 CPU工作7秒,空閒295秒
值日生小哥相當於select模型中的select功能,負責輪詢任務是否可以工作,如果可以則直接工作,否則繼續輪詢。在小A阻塞的300秒裡面,阿姨(CPU)沒有傻等,而是在服務後面的人,也就是小B和小C,所以這裡與模型3不同的是,這裡有5秒CPU是工作的。 如果打飯的人越多,這種模型CPU的利用率越高,例如如果有小D,小E,小F…… 等需要服務,CPU可以在小A阻塞的300秒期間內繼續服務其他人。實際上值日生小哥輪詢也會耗時,這個耗時是很少的,幾乎可以忽略不計,但是如果任務非常多,這個輪詢還是會影響效能的,但是epoll模型已經不使用輪詢的方式,相當於A,B,C會主動跟值日生小哥報告,說我準備好了,可以直接打菜了。
這種模式下使用者體驗好,CPU利用率高(任務越多利用率越高)
5. 單執行緒多工,有耗時計算
回到最開始的模型,如下:
阿姨:打一個菜需要1秒
小A:200個菜
小B:3個菜
小C:2個菜
順序做完所有任務,需要耗時 200 + 3 + 2 = 205秒, CPU無空閒,但是使用者體驗卻不是很好,因為顯然後面的 B,C 需要等待小A 200秒的時間,這種情況下是沒有IO阻塞的,但是任務A本身太耗CPU了,所以說如果單執行緒中出現了耗時的操作,一定會影響體驗(IO操作或者是耗時的計算都屬於耗時的操作,都會導致阻塞,但是這兩種導致阻塞的性質是不一樣的)。在所有的單執行緒模型中都不允許出現阻塞的情況,如果出現,那麼使用者體驗是極差的,例如在UI程式設計中(QT,C# Winform)是不允許在UI執行緒中做耗時的操作的,否則會導致UI介面無響應。 編寫Nodejs程式的時候,我們所寫的程式碼實際上是在一個執行緒中執行的,所以也不允許有阻塞的操作(當然整個Nodejs框架實現非同步,一定不止一個執行緒)。
出現阻塞的情況一般有2種,一種是IO阻塞,例如典型的如磁碟操作,這種情況下的阻塞會導致CPU空閒等待(當然現代作業系統中如果IO阻塞,作業系統一定會將導致IO阻塞的執行緒掛起)。這種阻塞的情況,可以通過非同步IO的方法避免,這樣就避免程式中僅有的單執行緒被作業系統掛起。另一種情況下是確實有非常多的計算操作,例如一個複雜的加密演算法,確實需要消耗非常多的CPU時間,這種情況下CPU並不是空閒的,反而是全負荷工作的。這種CPU密集的工作不適合放在單執行緒中,雖然CPU的利用率很高,但是使用者體驗並不是很好。這種情況下使用多執行緒反而會更好,例如如果3個任務,每個任務都在一個執行緒中,也就是有3個執行緒,A任務在ThreadA中,B任務在ThreadB中,C任務在ThreadC中,那麼即使A任務的計算量比較大,B,C兩個任務所在的執行緒也不必等待A任務完成之後再工作,他們也有機會得到排程,這是由作業系統來完成的。這樣就不會因為某一個任務計算量大,而導致阻塞其他任務而影響體驗了。
6. 多執行緒程式
我們將上面的模型改造成多執行緒的模型是怎樣的呢,我們在模型5的基礎上新增一個角色,管理員大叔(作業系統的角色):
阿姨:打一個菜需要1秒
小A:200個菜
小B:3個菜
小C:2個菜
加入管理員大叔之後變成這樣的了,小A打兩個菜之後,大叔說,你打的菜太多了,不能因為你要打200個菜,讓後面的同學都沒有機會打菜,你打兩個菜之後等一會,讓後面的同學也有機會。
大叔讓小B打兩個菜,然後讓小C打兩個菜(小C完成),然後再讓小A打兩個菜(完成之後小A總共就有4個菜了),再讓小B打1個菜(此時小B總共打3個菜,完成),然後小A打剩下的196個菜。
CPU的利用率:很高,阿姨在不斷的工作
使用者體驗:不錯,即使小A要打200個菜,小B,小C也有機會。 當然如果小A說我是幫校長打菜,要快一點(執行緒優先順序高),那也只能先把小A服務完
總耗時: 200 + 3 + 2 + (大叔指揮安排所消耗的時間,包括從小C切換回小A的時候,大叔要知道小A上次打的菜是哪兩個,這次應該接著打什麼菜,這相當於執行緒上下文切換的開銷以及執行緒環境的儲存與恢復),所以並不是執行緒越多越好,執行緒非常多的時候大叔估計會焦頭爛額吧,要記住這麼狀態,切換來切換去也耗時間。
這種模型下實際上是將小A的耗時任務,分成多份去執行而不是集中執行,所以小A要完成他的任務,可能需要更多的時間(期間他也需要等別人,阿姨不會一直為他一個人服務,但是阿姨為他服務的時間是沒有變化的),這種其實有點以時間換取使用者體驗(小B和小C的體驗,小A的體驗可能就不會那麼好了,但是小A本來也非常耗時,所以多等一會是不是也沒關係)
那麼IO阻塞和CPU計算耗時阻塞這兩者有什麼區別呢? 區別在於IO阻塞是不使用CPU的,而CPU計算耗時導致的阻塞是會使用CPU的。 例如上面的例子中,小A說忘記帶錢了需要同學送錢,於是小A等著同學送錢過來,這個過程中阿姨並沒有為小A提供服務,這個過程中為小A提供服務的是他的同學(送錢過來),實際上小A的同學相當於現代計算機系統中的DMA(直接記憶體操作),小A同學送錢的過程相當於DMA從磁碟讀取資料到記憶體的過程,這個過程基本不需要CPU干預。
當然在DMA技術還沒有出現的年代,從磁碟讀取檔案也是需要CPU傳送指令去讀取的,也就是說需要CPU的計算,應用到這裡的場景中,就是阿姨親自跑一趟幫小A把錢拿過來。
7. 多CPU
多CPU是一個更加複雜的問題,多CPU如何排程? 小A在第一個視窗打兩個菜,又跑到第二個視窗打兩個菜這種情況如何處理。小A在第一個視窗,小B在第二個視窗他們要同一個菜,但是這個菜只夠一個人,那麼兩個視窗阿姨如何分配這種需求(實際上應該是由作業系統也就是管理員大叔來決定如何分配,也就是多核下的執行緒同步與互斥)?
多核CPU情況下,多執行緒的排程,互斥,鎖與同步相對來講更加複雜,多核情況下是真正的並行,同一時刻有多個執行緒在同時執行,他們的競爭怎麼處理,多個CPU之間如何同步(多CPU之間的快取狀態一致性)等等一系列的問題。
8. 多執行緒與多程式
上面描述的多執行緒實際上是討論的是多執行緒的排程問題,這裡我們說一說多執行緒與多程式與資源的分配問題。什麼意思呢,一群人(多個執行緒)在一個桌子(程式)上吃飯,他們會涉及到一些問題,比如多個人可能會夾一個菜(競爭),A和B同時看到盤子裡面有一塊肉,同時伸出筷子去夾,A先夾走,B遲了一點伸到盤子的時候已經沒了,只能縮回來(臨界資源,互斥),有一個點心需要用饃夾肉一起吃。A夾了肉,B夾了饃,A需要B的饃,B需要A的肉,他們僵持不下誰都不讓步(死鎖)。
多執行緒之間的資源共享是非常方便的,因為他們共用程式的資源空間(在一個桌子上),但是需要注意一系列的問題,競爭,死鎖,同步等。如果在旁邊再開一個桌子(程式)。 那麼桌子之間講話,遞東西又不方便(程式間通訊),而開一個桌子的開銷比在一個桌子上多加一個人的開銷要大。另外一個桌子上的人數不可能無限制增加,桌子的容量有限也坐不下這麼多人(程式的執行緒控制程式碼是有限制的)。一個桌子壞了不會影響到另一個桌子上面人的就餐情況(程式間相互獨立,一個程式崩潰不會影響另一個),而一個桌子上的某人喝掛了需要送醫院,估計這一桌人都要散了(執行緒掛掉會導致整個程式也掛掉)。所以多執行緒與多程式是各有優缺點,不能一概而論。
說明:多執行緒桌子的比喻受到知乎使用者[pansz]的啟發,但是該比喻似乎說明不了執行緒同步的情況。
9. 總結
單執行緒程式:適合IO非同步,不能阻塞,不能有大量耗CPU的計算。典型如Nodejs,還有一些網路程式
多執行緒程式:適合CPU密集型程式
相關文章
- 執行緒與多執行緒執行緒
- 執行緒和執行緒池的理解與java簡單例子執行緒Java單例
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 多執行緒 -- 初學簡單例子執行緒單例
- 【多執行緒總結(二)-執行緒安全與執行緒同步】執行緒
- 多執行緒【執行緒池】執行緒
- 多執行緒--執行緒管理執行緒
- Java多執行緒——執行緒Java執行緒
- 多執行緒-執行緒控制之休眠執行緒執行緒
- 多執行緒-執行緒控制之加入執行緒執行緒
- 多執行緒-執行緒控制之禮讓執行緒執行緒
- 多執行緒-執行緒控制之中斷執行緒執行緒
- 多執行緒和多執行緒同步執行緒
- Java多執行緒1:程式與執行緒概述Java執行緒
- 多執行緒賣火車票簡單例子執行緒單例
- Python簡單實現多執行緒例子Python執行緒
- Java多執行緒學習(3)執行緒同步與執行緒通訊Java執行緒
- 多執行緒-執行緒控制之守護執行緒執行緒
- Java執行緒池使用說明Java執行緒
- 多執行緒之初識執行緒執行緒
- Java多執行緒-執行緒中止Java執行緒
- Java多執行緒——執行緒池Java執行緒
- 多執行緒-執行緒概述等執行緒
- 瀏覽器多執行緒和js單執行緒瀏覽器執行緒JS
- VC多執行緒 C++ 多執行緒執行緒C++
- 執行緒1-單執行緒執行緒
- 多執行緒Demo學習(執行緒的同步,簡單的執行緒通訊)執行緒
- 多執行緒與高併發(二)執行緒安全執行緒
- 併發與多執行緒之執行緒安全篇執行緒
- 細說C#多執行緒那些事:執行緒基礎C#執行緒
- 多執行緒與高併發(一)多執行緒入門執行緒
- GIL與多執行緒執行緒
- Notification與多執行緒執行緒
- Java多執行緒—執行緒同步(單訊號量互斥)Java執行緒
- redis為什麼用單執行緒不用多執行緒Redis執行緒
- Java多執行緒下載的例子。Java執行緒
- java 多執行緒守護執行緒Java執行緒
- Java多執行緒-執行緒通訊Java執行緒