在我看來單從程序的角度來看,一個好的程序的目標應該是性能與用戶體驗的平衡。固然一個程序是否可以知足用戶的需求暫且不談,這是業務層面的問題,咱們僅僅討論程序自己。圍繞兩點來展開,性能與用戶體驗。
性能:在其餘同等條件下,高性能的程序應該能夠等同於CPU的利用率,CPU的利用率越高(一直在工做,沒有閒下來的時候),程序的性能越高。
體驗:這裏的體驗不僅是界面多麼漂亮,功能多麼順手,這裏的體驗指程序的響應速度,響應速度越快,用戶體驗越好。
下面咱們就這兩點進行各類模型的討論。算法
以生活中食堂打飯的場景做爲比喻,假設有這樣的場景,小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是滿負荷不間斷運轉的,沒有空閒,用戶體驗還不錯。這種程序中每一個任務的耗時都比較小,是很是理想的狀態,通常狀況下基本不太可能存在。編程
將上面的場景稍微作改動:
阿姨:打一個菜須要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空閒。因此單線程中不要有阻塞出現。緩存
仍是上面的模型,加入一個角色:值日生小哥,他負責事先詢問每個人是否帶錢了,若是帶錢了則容許打菜,不然把錢準備好了再說。
<1> 值日生小哥問小A準備好打菜了嗎,小A說忘帶錢了,值日生小哥說,你把錢準備好了再說,小A開始準備(須要300秒,今後刻開始記時)。
<2> 值日生小哥問小B準備好打菜了嗎,小B說能夠了,阿姨服務小B,耗時3秒 (與此同時小A還在準備中,而且已經準備了3秒)
<3> 值日生小哥問小C準備好打菜了嗎,小C說能夠了,阿姨服務小C,耗時2秒 (與此同時小A還在準備中,而且已經準備了5秒,前面的3秒+這裏的2秒)
<4> 值日生小哥問小A準備好了沒有,小A說還要等一會,阿姨因爲沒有人過來服務,處於空閒狀態 (小A還在準備中,他還須要準備295秒,可是這個時候B和C已經服務完了)
<5> 從第1步開始計時有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利用率高(任務越多利用率越高)網絡
回到最開始的模型,以下:
阿姨:打一個菜須要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任務完成以後再工做,他們也有機會獲得調度,這是由操做系統來完成的。這樣就不會由於某一個任務計算量大,而致使阻塞其餘任務而影響體驗了。多線程
咱們將上面的模型改形成多線程的模型是怎樣的呢,咱們在模型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把錢拿過來。異步
多CPU是一個更加複雜的問題,多CPU如何調度? 小A在第一個窗口打兩個菜,又跑到第二個窗口打兩個菜這種狀況如何處理。小A在第一個窗口,小B在第二個窗口他們要同一個菜,可是這個菜只夠一我的,那麼兩個窗口阿姨如何分配這種需求(實際上應該是由操做系統也就是管理員大叔來決定如何分配,也就是多核下的線程同步與互斥)?
多核CPU狀況下,多線程的調度,互斥,鎖與同步相對來說更加複雜,多核狀況下是真正的並行,同一時刻有多個線程在同時運行,他們的競爭怎麼處理,多個CPU之間如何同步(多CPU之間的緩存狀態一致性)等等一系列的問題。性能
上面描述的多線程其實是討論的是多線程的調度問題,這裏咱們說一說多線程與多進程與資源的分配問題。什麼意思呢,一羣人(多個線程)在一個桌子(進程)上吃飯,他們會涉及到一些問題,好比多我的可能會夾一個菜(競爭),A和B同時看到盤子裏面有一塊肉,同時伸出筷子去夾,A先夾走,B遲了一點伸到盤子的時候已經沒了,只能縮回來(臨界資源,互斥),有一個點心須要用饃夾肉一塊兒吃。A夾了肉,B夾了饃,A須要B的饃,B須要A的肉,他們僵持不下誰都不讓步(死鎖)。
多線程之間的資源共享是很是方便的,由於他們共用進程的資源空間(在一個桌子上),可是須要注意一系列的問題,競爭,死鎖,同步等。若是在旁邊再開一個桌子(進程)。 那麼桌子之間講話,遞東西又不方便(進程間通訊),而開一個桌子的開銷比在一個桌子上多加一我的的開銷要大。另一個桌子上的人數不可能無限制增長,桌子的容量有限也坐不下這麼多人(進程的線程句柄是有限制的)。一個桌子壞了不會影響到另外一個桌子上面人的就餐狀況(進程間相互獨立,一個進程崩潰不會影響另外一個),而一個桌子上的某人喝掛了須要送醫院,估計這一桌人都要散了(線程掛掉會致使整個進程也掛掉)。因此多線程與多進程是各有優缺點,不能一律而論。加密
說明:多線程桌子的比喻受到知乎用戶[pansz]的啓發,可是該比喻彷佛說明不了線程同步的狀況。 spa
單線程程序:適合IO異步,不能阻塞,不能有大量耗CPU的計算。典型如Nodejs,還有一些網絡程序
多線程程序:適合CPU密集型程序
若是您以爲這篇文章對您有幫助,須要您的【贊】,讓更多的人也能看見哦