以生活的例子說明單線程和多線程

以生活例子說明單線程與多線程

轉載自:書圈

1. 程序設計的目標

在我看來單從程序的角度來看,一個好的程序的目標應該是性能與用戶體驗的平衡。當然一個程序是否能夠滿足用戶的需求暫且不談,這是業務層面的問題,我們僅僅討論程序本身。圍繞兩點來展開,性能與用戶體驗。

性能:高性能的程序應該可以等同於CPU的利用率,CPU的利用率越高(一直在工作,沒有閒下來的時候),程序的性能越高。
體驗:這裏的體驗不只是界面多麼漂亮,功能多麼順手,這裏的體驗指程序的響應速度,響應速度越快,用戶體驗越好。

下面我們就這兩點進行各種模型的討論。

2. 單線程多任務無阻塞

以生活中食堂打飯的場景作爲比喻,假設有這樣的場景,小A,小B,小C 在窗口依次排隊打飯。 假設窗口負責打飯的阿姨打一個菜需要耗時1秒。如果小A需要2個菜,小B需要3個菜,小C需要2個菜。如下:

阿姨(CPU):打一個菜需要1秒
小A:2個菜
小B:3個菜
小C:2個菜

那麼在這種模型下將所有服Wu做完阿姨需要耗時 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秒。

所有服Wu做完花費 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的肉,他們僵持不下誰都不讓步(死鎖)。

多線程之間的資源共享是非常方便的,因爲他們共用進程的資源空間(在一個桌子上),但是需要注意一系列的問題,競爭,死鎖,同步等。如果在旁邊再開一個桌子(進程)。 那麼桌子之間講話,遞東西又不方便(進程間通信),而開一個桌子的開銷比在一個桌子上多加一個人的開銷要大。另外一個桌子上的人數不可能無限制增加,桌子的容量有限也坐不下這麼多人(進程的線程句柄是有限制的)。一個桌子壞了不會影響到另一個桌子上面人的就餐情況(進程間相互DuLi,一個進程崩潰不會影響另一個),而一個桌子上的某人喝掛了需要送醫院,估計這一桌人都要散了(線程掛掉會導致整個進程也掛掉)。所以多線程與多進程是各有優缺點,不能一概而論。

說明:多線程桌子的比喻受到知乎用戶[pansz]的啓發,但是該比喻似乎說明不了線程同步的情況。

9. 總結

單線程程序:適合IO異步,不能阻塞,不能有大量耗CPU的計算。典型如Nodejs,還有一些網絡程序

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章