<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Others on Wayne's blog | 偉恩的部落格 | 技術博客</title><link>https://blog.wayne-project.com/tags/others/</link><description>Recent content in Others on Wayne's blog | 偉恩的部落格 | 技術博客</description><generator>Hugo -- gohugo.io</generator><language>zh-tw</language><lastBuildDate>Sat, 07 Jan 2023 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.wayne-project.com/tags/others/index.xml" rel="self" type="application/rss+xml"/><item><title>以生活的例子說明單線程和多線程</title><link>https://blog.wayne-project.com/2023-01-07/single-multi-thread/</link><pubDate>Sat, 07 Jan 2023 00:00:00 +0000</pubDate><guid>https://blog.wayne-project.com/2023-01-07/single-multi-thread/</guid><description>&lt;img src="https://live.staticflickr.com/65535/52611539777_c564013cbd_o.png" alt="Featured image of post 以生活的例子說明單線程和多線程" /&gt;&lt;style&gt;
.article-content p code {
background-color: #f5f5f5;
color: #ff3860;
}
.focus {
background: #f1e2e2;
color: #d62c2c;
padding: 0 5px;
}
&lt;/style&gt;
&lt;p&gt;&lt;a class="link" href="https://samknows.one/hc/en-gb/articles/115003164305-What-is-the-difference-between-Single-and-Multi-Thread-" target="_blank" rel="noopener"
&gt;參考網站&lt;/a&gt;&lt;br&gt;
&lt;a class="link" href="https://www.twblogs.net/a/5b8d26df2b717718833be03a" target="_blank" rel="noopener"
&gt;參考網站&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;以生活例子說明單線程與多線程。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="程序設計的目標"&gt;程序設計的目標&lt;/h2&gt;
&lt;p&gt;在我看來單從程序的角度來看，一個好的程序的目標應該是性能與用戶體驗的平衡。當然一個程序是否能夠滿足用戶的需求暫且不談，這是業務層面的問題，我們僅僅討論程序本身。圍繞兩點來展開，性能與用戶體驗。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;性能：高性能的程序應該可以等同於CPU的利用率，CPU的利用率越高(一直在工作，沒有閒下來的時候)，程序的性能越高。&lt;/li&gt;
&lt;li&gt;體驗：這裏的體驗不只是界面多麼漂亮，功能多麼順手，這裏的體驗指程序的響應速度，響應速度越快，用戶體驗越好。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面我們就這兩點進行各種模型的討論。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="單線程多任務無阻塞"&gt;單線程多任務無阻塞&lt;/h2&gt;
&lt;p&gt;以生活中食堂打飯的場景作爲比喻，假設有這樣的場景，小A，小B，小C 在窗口依次排隊打飯。&lt;/p&gt;
&lt;p&gt;假設窗口負責打飯的阿姨打一個菜需要耗時1秒。如果小A需要2個菜，小B需要3個菜，小C需要2個菜。如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿姨(CPU)：打一個菜需要1秒&lt;/li&gt;
&lt;li&gt;小A：2個菜&lt;/li&gt;
&lt;li&gt;小B：3個菜&lt;/li&gt;
&lt;li&gt;小C：2個菜&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;那麼在這種模型下將所有服務做完阿姨需要耗時 2 + 3 + 2 = &lt;strong&gt;7秒&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;阿姨 = CPU，小A、小B、小C = 任務(這裏是以任務爲概念，表示需要做一些事情)&lt;/p&gt;
&lt;p&gt;這種模型下CPU是滿負荷不間斷運轉的，沒有空閒，用戶體驗還不錯。&lt;/p&gt;
&lt;p&gt;這種程序中每個任務的耗時都比較小，是非常理想的狀態，一般情況下基本不太可能存在。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="單線程多任務io阻塞"&gt;單線程多任務IO阻塞&lt;/h2&gt;
&lt;p&gt;將上面的場景稍微做改動：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿姨：打一個菜需要1秒&lt;/li&gt;
&lt;li&gt;小A：2個菜，但是忘記帶錢了，要找同學送過來，估計需要等5分鐘可以送到(可以理解爲磁盤IO)&lt;/li&gt;
&lt;li&gt;小B：3個菜&lt;/li&gt;
&lt;li&gt;小C：2個菜&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;這種情況下小A這裏發生了阻塞，實際上小A這裏耗費了5分鐘也就是 300秒 + 2個菜的時間，也就是302秒，而CPU則空閒了300秒，實際上工作2秒。&lt;/p&gt;
&lt;p&gt;所有服務做完花費 302 + 3 + 2 = &lt;strong&gt;307秒&lt;/strong&gt;，CPU實際工作7秒，等待300秒。極大浪費了CPU的時鐘週期。&lt;/p&gt;
&lt;p&gt;用戶體驗很差，因爲小A阻塞的時候，後面的所有人都等着，而實際上此時CPU空閒。所以單線程中不要有阻塞出現。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="單線程多任務異步io"&gt;單線程多任務異步IO&lt;/h2&gt;
&lt;p&gt;還是上面的模型，加入一個角色：值日生小哥，他負責事先詢問每一個人是否帶錢了，如果帶錢了則允許打菜，否則把錢準備好了再說。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;值日生小哥問小A準備好打菜了嗎，小A說忘帶錢了，值日生小哥說，你把錢準備好了再說，小A開始準備(需要300秒，從此刻開始記時)。&lt;/li&gt;
&lt;li&gt;值日生小哥問小B準備好打菜了嗎，小B說可以了，阿姨服務小B，耗時3秒。&lt;/li&gt;
&lt;li&gt;值日生小哥問小C準備好打菜了嗎，小C說可以了，阿姨服務小C，耗時2秒。&lt;/li&gt;
&lt;li&gt;值日生小哥問小A準備好了沒有，小A說還要等一會，阿姨由於沒有人過來服務，處於空閒狀態。&lt;/li&gt;
&lt;li&gt;300秒之後，小A準備好了，阿姨服務小A，耗時2秒。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整個過程做完耗時 300 + 2 = &lt;strong&gt;302秒&lt;/strong&gt;，CPU工作7秒，空閒295秒。&lt;/p&gt;
&lt;p&gt;值日生小哥相當於select模型中的select功能，負責輪詢任務是否可以工作，如果可以則直接工作，否則繼續輪詢。&lt;/p&gt;
&lt;p&gt;在小A阻塞的300秒裏面，阿姨(CPU)沒有傻等，而是在服務後面的人，也就是小B和小C，所以這裏與模型3不同的是，這裏有5秒CPU是工作的。&lt;/p&gt;
&lt;p&gt;如果打飯的人越多，這種模型CPU的利用率越高，例如如果有小D，小E，小F&amp;hellip;等需要服務，CPU可以在小A阻塞的300秒期間內繼續服務其他人。實際上值日生小哥輪詢也會耗時，這個耗時是很少的，幾乎可以忽略不計，但是如果任務非常多，這個輪詢還是會影響性能的，但是epoll模型已經不使用輪詢的方式，相當於A，B，C會主動跟值日生小哥報告，說我準備好了，可以直接打菜了。&lt;/p&gt;
&lt;p&gt;這種模式下用戶體驗好，CPU利用率高(任務越多利用率越高)&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="單線程多任務有耗時計算"&gt;單線程多任務，有耗時計算&lt;/h2&gt;
&lt;p&gt;回到最開始的模型，如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿姨：打一個菜需要1秒&lt;/li&gt;
&lt;li&gt;小A：200個菜&lt;/li&gt;
&lt;li&gt;小B：3個菜&lt;/li&gt;
&lt;li&gt;小C：2個菜&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;順序做完所有任務，需要耗時 200 + 3 + 2 = 205秒，CPU無空閒，但是用戶體驗卻不是很好，因爲顯然後面的 B、C 需要等待小A 200秒的時間，這種情況下是沒有IO阻塞的，但是任務A本身太耗CPU了，所以說如果單線程出現了耗時的操作，一定會影響體驗(IO操作或者是耗時的計算都屬於耗時的操作，都會導致阻塞，但是這兩種導致阻塞的性質是不一樣的)。&lt;/p&gt;
&lt;p&gt;在所有的單線程模型中都不允許出現阻塞的情況，如果出現，那麼用戶體驗是極差的，例如在UI編程中(QT、C# Winform)是不允許在UI線程中做耗時的操作的，否則會導致UI界面無響應。 編寫Nodejs程序的時候，我們所寫的代碼實際上是在一個線程中執行的，所以也不允許有阻塞的操作(當然整個Nodejs框架實現異步，一定不止一個線程)。&lt;/p&gt;
&lt;p&gt;出現阻塞的情況一般有2種，一種是&lt;strong&gt;IO阻塞&lt;/strong&gt;，例如典型的如磁盤操作，這種情況下的阻塞會導致CPU空閒等待(當然現代操作系統中如果IO阻塞，操作系統一定會將導致IO阻塞的線程掛起)。這種阻塞的情況，可以通過異步IO的方法避免，這樣就避免程序中僅有的單線程被操作系統掛起。另一種情況下是確實有&lt;strong&gt;非常多的計算操作&lt;/strong&gt;，例如一個複雜的加密算法，確實需要消耗非常多的CPU時間，這種情況下CPU並不是空閒的，反而是全負荷工作的。&lt;/p&gt;
&lt;p&gt;這種CPU密集的工作不適合放在單線程中，雖然CPU的利用率很高，但是用戶體驗並不是很好。這種情況下使用多線程反而會更好，例如如果3個任務，每個任務都在一個線程中，也就是有3個線程，A任務在ThreadA中，B任務在ThreadB中，C任務在ThreadC中，那麼即使A任務的計算量比較大，B，C兩個任務所在的線程也不必等待A任務完成之後再工作，他們也有機會得到調度，這是由操作系統來完成的。這樣就不會因爲某一個任務計算量大，而導致阻塞其他任務而影響體驗了。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="多線程程序"&gt;多線程程序&lt;/h2&gt;
&lt;p&gt;我們將上面的模型改造成多線程的模型是怎樣的呢，我們在模型5的基礎上添加一個角色，管理員大叔(操作系統的角色)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阿姨：打一個菜需要1秒&lt;/li&gt;
&lt;li&gt;小A：200個菜&lt;/li&gt;
&lt;li&gt;小B：3個菜&lt;/li&gt;
&lt;li&gt;小C：2個菜&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加入管理員大叔之後變成這樣的了，小A打兩個菜之後，大叔說，你打的菜太多了，不能因爲你要打200個菜，讓後面的同學都沒有機會打菜，你打兩個菜之後等一會，讓後面的同學也有機會。&lt;/p&gt;
&lt;p&gt;大叔讓小B打兩個菜，然後讓小C打兩個菜(小C完成)，然後再讓小A打兩個菜(完成之後小A總共就有4個菜了)，再讓小B打1個菜(此時小B總共打3個菜，完成)，然後小A打剩下的196個菜。&lt;/p&gt;
&lt;p&gt;CPU的利用率：很高，阿姨在不斷的工作。&lt;/p&gt;
&lt;p&gt;用戶體驗：不錯，即使小A要打200個菜，小B，小C也有機會；當然如果小A說我是幫校長打菜，要快一點(&lt;strong&gt;線程優先級高&lt;/strong&gt;)，那也只能先把小A服務完。&lt;/p&gt;
&lt;p&gt;總耗時： 200 + 3 + 2 + (大叔指揮安排所消耗的時間，包括從小C切換回小A的時候，大叔要知道小A上次打的菜是哪兩個，這次應該接着打什麼菜，這相當於線程上下文切換的開銷以及線程環境的保存與恢復)，所以並不是線程越多越好，線程非常多的時候大叔估計會焦頭爛額吧，要記住這麼狀態，切換來切換去也耗時間。&lt;/p&gt;
&lt;p&gt;這種模型下實際上是將小A的耗時任務，&lt;strong&gt;分成多份去執行&lt;/strong&gt;而不是集中執行，所以小A要完成他的任務，可能需要更多的時間(期間他也需要等別人，阿姨不會一直爲他一個人服務，但是阿姨爲他服務的時間是沒有變化的)，這種其實有點以時間換取用戶體驗(小B和小Ｃ的體驗，小Ａ的體驗可能就不會那麼好了，但是小Ａ本來也非常耗時，所以多等一會是不是也沒關係)。&lt;/p&gt;
&lt;p&gt;那麼IO阻塞和CPU計算耗時阻塞這兩者有什麼區別呢？&lt;/p&gt;
&lt;p&gt;區別在於IO阻塞是不使用CPU的，而CPU計算耗時導致的阻塞是會使用CPU的。&lt;/p&gt;
&lt;p&gt;例如上面的例子中，小A說忘記帶錢了需要同學送錢，於是小A等着同學送錢過來，這個過程中阿姨並沒有爲小A提供服務，這個過程中爲小A提供服務的是他的同學(送錢過來)，實際上小A的同學相當於現代計算機系統中的DMA(直接內存操作)，小A同學送錢的過程相當於DMA從磁盤讀取數據到內存的過程，這個過程基本不需要CPU干預。&lt;/p&gt;
&lt;p&gt;當然在DMA技術還沒有出現的年代，從磁盤讀取文件也是需要CPU發送指令去讀取的，也就是說需要CPU的計算，應用到這裏的場景中，就是阿姨親自跑一趟幫小A把錢拿過來。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="多cpu"&gt;多CPU&lt;/h2&gt;
&lt;p&gt;多CPU是一個更加複雜的問題，多CPU如何調度？小A在第一個窗口打兩個菜，又跑到第二個窗口打兩個菜這種情況如何處理？&lt;/p&gt;
&lt;p&gt;小A在第一個窗口，小B在第二個窗口他們要同一個菜，但是這個菜只夠一個人，那麼兩個窗口阿姨如何分配這種需求(實際上應該是由操作系統也就是管理員大叔來決定如何分配，也就是多核下的線程同步與互斥)？&lt;/p&gt;
&lt;p&gt;多核CPU情況下，多線程的調度、互斥、鎖與同步相對來講更加複雜，多核情況下是真正的並行，同一時刻有多個線程在同時運行，他們的競爭怎麼處理，多個CPU之間如何同步(多CPU之間的緩存狀態一致性)等等一系列的問題。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="多線程與多進程"&gt;多線程與多進程&lt;/h2&gt;
&lt;p&gt;上面描述的多線程實際上是討論的是多線程的調度問題，這裏我們說一說多線程與多進程與資源的分配問題。什麼意思呢，一群人(多個線程)在一個桌子(進程)上吃飯，他們會涉及到一些問題，比如多個人可能會夾一個菜(競爭)，A和B同時看到盤子裏面有一塊肉，同時伸出筷子去夾，A先夾走，B遲了一點伸到盤子的時候已經沒了，只能縮回來(臨界資源，互斥)，有一個點心需要用饃夾肉一起吃。A夾了肉，B夾了饃，A需要B的饃，B需要A的肉，他們僵持不下誰都不讓步(死鎖)。&lt;/p&gt;
&lt;p&gt;多線程之間的資源共享是非常方便的，因爲他們共用進程的資源空間(在一個桌子上)，但是需要注意一系列的問題：競爭、死鎖、同步&amp;hellip;等。如果在旁邊再開一個桌子(進程)。 那麼桌子之間講話，遞東西又不方便(進程間通信)，而開一個桌子的開銷比在一個桌子上多加一個人的開銷要大。另外一個桌子上的人數不可能無限制增加，桌子的容量有限也坐不下這麼多人(進程的線程句柄是有限制的)。一個桌子壞了不會影響到另一個桌子上面人的就餐情況(進程間相互DuLi，一個進程崩潰不會影響另一個)，而一個桌子上的某人喝掛了需要送醫院，估計這一桌人都要散了(線程掛掉會導致整個進程也掛掉)。所以多線程與多進程是各有優缺點，不能一概而論。&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="總結"&gt;總結&lt;/h2&gt;
&lt;p&gt;單線程程序：適合IO異步，不能阻塞，不能有大量耗CPU的計算，典型如Nodejs，還有一些網絡程序。&lt;/p&gt;
&lt;hr&gt;</description></item></channel></rss>