撬開多線程的大門——學習多線程必須掌握的基本概念

1.進程

進程的概念從字義上理解相對還是比較抽象的,但進程實際上對我們並不陌生,可以說它無時不刻的伴隨着我們的生活。當你每天上班打開電腦,運行微信與好友通訊、運行瀏覽器閱讀網頁新聞等,這一些將程序運行起來的操作,都屬於創建了一個進程。並且我們可以對同一種程序重複運行多次,這意味着一個程序可以創建多個進程,例如我們時常針對Word這一種程序,反覆的運行從而閱讀不同的文檔。

根據我們日常生活中對程序使用的場景而言,我們可以通俗的將進程理解爲:進程就是運行起來了的程序;進程是程序的一段執行過程;進程是一個正在執行的程序;進程是程序的實例。程序是靜態的,通過運行程序就會產生動態的進程。總之,諸如此類。

正式地說,進程是一個操作系統級別的概念,進程是源於一個具有獨立功能的程序,與指令、數據集合的一次運行活動。它是操作系統動態執行任務的基本單元,是操作系統進行資源分配的基本單位。在這一點上就類似於軍事戰役,司令就像操作系統,它不會對某個士兵下達命令或分配物資,而是以部隊爲單位下達命令並分配物資,調度各種部隊來指揮作戰,這裏部隊的調度分配就有點類似於進程。

從結構上,我們可以想象操作系統是間大房子,衆多程序運行同在一件大房子裏,如果沒有隔離的房間,勢必會錯亂不堪。而進程會起到類似“房間”隔離的作用,讓操作系統的運行環境更加穩定,即使一個程序失敗也不會影響另一個程序。從這一點上,我們可以認爲,進程提供了程序執行的獨立環境和安全邊界。

正在運行的操作系統(你現在的電腦)就是由各種進程的活動構成,你可以打開“任務管理器”,可以瞭解你當前計算機的所有進程,以及進程的資源分配情況。


2.線程

根據上文中的介紹,總而言之,我們可以將進程看作是一個正在運行的程序。既然是運行的程序,必定會對程序有所期許(指示/任務)。試想下,你打開某個程序使它運行是爲了什麼,你如果你打開“QQ音樂”肯定希望它播放一首你喜歡的歌曲,你如果打開“餓了麼”你肯定你希望點一份外賣。對於以上這些,你對程序下達的“指示/任務”,實際上投射到程序當中,就會對應產生一條線程。這一點可以說明,線程是進程運行過程中執行的任務。

一個程序的運行對於用戶而言,往往感知不到代碼執行的存在,用戶通常實現某個功能就點擊相應的按鈕。實際上,在點擊按鈕的背後,進程不光會產生一個線程,線程會根據對應的操作選擇執行一條代碼的路線,通過執行這條代碼路線來實現相應的功能。

我們可以將充斥代碼的程序想象成一幅旅遊地圖,地圖上不同的旅遊線路就像代碼中不同的分支,我們選擇不同的旅遊線路就相當於選擇不同的代碼分支,通過不同的線路就可以到達不一樣的景點。程序的執行也是如此,用戶選擇執行不同的操作,進程中就會創建不同的代碼執行路線,線程會根據相應的路線執行代碼,從而實現相應的功能。所以從執行層面,我們可以將線程理解成一條代碼的執行路徑

每一個線程都運行在一個操作系統的進程中,因此進程可以看作是線程的容器。每一個進程都必定包含一個用做程序入口點的主線程,該主線程會在程序運行起來時自動創建。除主線程之外,每個進程還可以通過編程方式創建額外的次線程(工作者線程),無論是主線程還是次線程都屬於進程中的一個獨立執行單元,並且在多個線程之間它們能夠同時訪問進程中的共享數據。


 3.併發

讓我們回溯到計算機CPU發展早期的時候,那時計算機的CPU都是單核的,並且一個單核的CPU在某一時刻只能執行一個線程。如果需要執行其他的線程,CPU只能等待當前線程執行結束之後,才能執行下一個線程。也就是說你打開了一個音樂程序(進程)播放周杰倫的“七里香”(線程),如果還需要打開記事本程序進行打字,則必須要等到“七里香”這首歌播放結束後才能進行。

上述的等待是CPU在忙着播放美妙的音樂,可能部分人還願意接受,可是某些等待是無意義的。例如,你將移動硬盤的資源拷貝到計算機的硬盤時,計算機幹活的重心會轉交給硬盤,它會發出大量I/O指令進行讀寫操作。然而讀寫操作通常是比較耗時的,如果CPU想要在這時進行其他的任務處理,則必須要等待硬盤操作完成後才能進行,這就導致CPU經常處於空閒狀態

上述說明了CPU在早期發展時的不足之處,於是人們爲了滿足多應用同時使用的需求,爲了提高CPU利用率,從而研究出了一種CPU併發工作的方式。計算機的很多概念,其實都可以在生活中找到影子,併發也是如此。想想你在工作時,一邊聽着音樂一邊打字的樣子;想想你在午餐時,一手拿着手機一手拿筷子往嘴裏塞事物的樣子;以上的這些現象就屬於併發,即在同一個事物,在同一個時間階段內,開展多項任務。

下面來說說併發的工作方式。我們可以將CPU執行的任務看作是線程,一個單核CPU在同一時間階段內,開展多個線程處理,就體現出了併發。具體來說,操作系統會使用一種算法對線程進行調度,促使將一個CPU的資源可以合理地分配給多個線程(任務),其中每個線程都將分配一段,CPU爲其執行的時間片,CPU會在多個線程之間不斷切換輪流的執行多個線程,也就是這個任務根據分配的時間片執行一會兒(10ms),在切換到另一個任務根據相應時間片再執行一會兒(10ms)。下圖展示了兩個線程併發執行的過程:

由於併發的方式促使線程切換速度很快,所以併發的執行通常對於用戶的感覺而言,就像是多個任務並行一樣,以致於產生了一種多個任務同一時間執行的假象。這一點聽上去可能有點是是而非,你需要將併發的多個任務看作是在同一個時間階段內執行的,而非是某個具體的時間點同時執行的。例如,兩個任務都是在0到60秒這個階段中完成的,但如果比較具體的執行時間點,一個任務某個執行時間點是08:30:12,另一個任務某個執行時間點則會是08:30:56。說白了就是,併發就是“一心二用”。這是因爲單核CPU的計算機並沒有能力在同一時間點運行多個線程。

併發的體現源於單核CPU資源合理的分配,促使多個線程在同一時間階段內開展,並且有效避免了CPU被某個線程長期霸佔的問題,提升了CPU資源利用率。但是,這種方式依然存在弊端。如果單核CPU處理的線程過多,CPU則會花費大量時間在這些線程之間進行切換,這會導致程序的性能下降。


4.並行

基於單核CPU的短板,並隨着計算機硬件的發展,CPU邁入了多核時代,雙核、四核、八核已屢見不鮮,甚至還有高達幾十核的CPU。多核CPU不在侷限於和單核CPU那種“一心兩用”的工作方式,而是可以真正實現同一時間點執行多個線程(任務),達到“雙管齊下”的效果。多核CPU的每個核心都可以獨立地執行一個線程,並且多個核心之間不會相互干擾。因此,多核CPU在不同的核心上,在同一時間點,分別執行一個任務的這種方式,稱之爲並行。

例如,同樣是執行兩個任務,雙核 CPU 的工作狀態如下圖所示:

 

通過上圖我們可以看出,雙核CPU可以在同一個時間各自執行一個任務,和單核CPU在兩個任務之間不斷切換相比,它的執行效率更高。需要注意的是,上圖中CPU的核數與線程(任務)數剛好匹配,這是個理想狀態,如果線程數大於了CPU的核數,那麼計算機會按照什麼樣的方式執行呢?你可以思考一番,然後在下文中找到答案。


5.併發&並行

在實際的情況中,我們的計算機或智能手機,通常都會同時處理幾百上千個線程(任務),並且對於目前的硬件條件而言,CPU具備的核數還無法普及或達到一個非常高的數值,所以線程(任務)數大於CPU核數是一個常態化的現象,對於這一點你可以打開任務管理器,通過查看CPU線程數就可以證實。

所以對於現實中存在的這種情況(核數低於線程數),計算機這個時候對於線程的處理,會同時存在併發和並行兩種情況:所有的CPU核心都會並行工作,其中每個核心還會進行併發工作。例如一個雙核 CPU 要執行四個任務,它的工作狀態如圖所示:

 

上圖中每個核心併發執行了兩個線程,兩個核心並行就執行了四個任務。當然也可以一個核心執行一個任務,另一個核心併發執行三個任務,具體的分配還是要取決於操作系統的調度算法,以及每個線程的處理狀態。

小結

併發的工作方式,會在單核CPU處理多個線程時出現,它代表了單核CPU交替執行不同線程的能力。並行的工作方式,只會在多核CPU處理的硬件條件下,並且線程數與核數相等情況下出現,它代表了多個核心同時執行多個任務的能力。在多核CPU中,併發核並行通常都會同時存在,兩種工作方式的結合,會有效的提升計算機執行程序的效率。


6.概念類比

不要被枯燥、籠統的概念所嚇跑。以上講解的知識點都屬於計算機的基本概念,初學者往往在讀完這些概念後都會感覺比較模糊,這是正常的。其實計算機中大部分的概念,都可以通過生活中的場景進行類比。上文中講解的概念也是如此。所以,接下來我將基於上文中的進程、線程、併發、並行的概念一起串一串,將它們融入到生活場景中進行類比。讓這些概念可以形象化的展示在你面前。

 

進程

我們首先將應用程序看作是一家飯店。飯店通常在沒有客人光臨之前,都是一個相對靜止的狀態,這一點也和未啓動的程序一樣。當某個家庭來到飯店解決晚餐時,此時靜態的飯店開始張燈結綵的歡迎客人了,此情此景可以看作一個程序開始啓動了,而這個家庭來到飯店進行晚餐的活動,就類似於程序開啓了一個進程

 

線程

程序通過開啓進程活動了起來,那麼活動的進程中必定會開展相應的任務。這就等於你飯店在安置後客人就坐後,需要爲客人烹製美味菜餚。我們來看看這個家庭點的菜餚:魚香肉絲、湖北藕湯、剁椒魚頭、夫妻肺片。這些菜餚的製作通常對於一個標準化的飯店而言,都會在廚房中會劃分不同的製作區域。例如,魚香肉絲要在竈臺區域、剁椒魚頭要在蒸櫃區域。

當廚房要烹製某個菜品時,廚師就會根據菜餚的製作類型(炒、湯、蒸),到達指定的區域進行烹飪。對於這個現象,就像程序爲實現不同的功能,會選擇不同的代碼路徑執行一樣。不同的菜餚要找到相應的區域進行烹製,並且爲客人制作菜餚是飯店的主要任務,綜上所述,我們可以將飯店爲客人制作菜餚的任務,看作是進程中執行的線程。

 

併發

先彆着急流口水,在點菜之後,我們將目光轉向廚房。此時的廚房只有一名廚師在崗,所以他一個人將面臨多道菜的製作,這名廚師並不打算一道菜一道菜的製作,因爲他擔心如果上菜太慢會導致:1.客人在喫前幾道菜時就飽了,客人會放棄後面的菜;2.由於菜的間隔時間過長,最後一道菜上時第一道菜就已經涼了。所以他打算一個人先同時開展兩道菜的製作,於是他在藕湯進行煨煮時,起鍋燒油去鍋裏炒魚香肉絲;當魚香肉絲燒至入味時,去給藕湯進行調味,利用兩個菜餚的空擋不斷切換,最後當魚香肉絲出鍋時,藕湯也已經煨好了。以上這名廚師同一時間階段內做多道菜的方式,就類似於與併發。

 

並行

此時老闆來到廚房,看到你非常賣力的爲客人準備菜餚,加上客人也有點着急,於是老闆穿上了白大褂,戴起來高帽子,打算加入你的行列。此時的廚房就已經有兩名廚師了,此時客人還只剩兩道菜(剁椒魚頭、夫妻肺片)沒有上。顯然目前最佳的製作方式就是,兩個廚師同時進行菜餚的製作,每個人負責一道菜餚。那麼對於以上兩名廚師同時進行菜餚的工作方式,就類似於並行。

 

併發&並行

在剛剛完成好上一桌客人的菜餚製作後,此時飯店又來了一桌客人,由於這桌客人聚餐的性質是公司聚餐,所以這桌客人點的菜餚有十幾道菜。此時的廚房只有兩名廚師,這就產生了一種情況:菜餚數大於廚師數,這也和線程數大於CPU核數同理。所以此時飯店的最佳工作方式就是,讓兩名廚師同時做菜,並且一個人負責多道菜的製作。對於這種情況,就類似CPU同時使用併發加並行兩種方式開展任務。


 7.多線程編程

多線程的實現可以從硬件或軟件上體現。在硬件上,計算機基於單核CPU的併發或多核CPU並行的工作方式,並結合操作系統的線程調度程序,就可以實現多線程處理。在軟件上,應用程序可以使用編程語言實現多線程的編碼,從而實現在一個進程中創建多個線程,來完成一個程序中多項任務的同時處理。

 

目的

使用多線程的目的是爲了同步完成多項任務。你可以試想下,你正要籌備一場年夜飯的食材。如果採購各式各樣的食材全都是你一個人去完成,那麼年夜飯的準備時長和開飯時間必定會延長。如果你安排你的家人進行協作,那麼你的家人可以和你同時去購買不同的食材,這樣一來會有助於節省你購買食材的時間。在這個例子中,你安排家人協作你購買食材,實際上就和編程中使用多線程的目的是一致的,編程中通過多線程會助於改善程序的總體響應性。

 

切換

多線程是把雙刃劍,不是越多越好。每一個線程都需要分配獨立的堆棧空間(耗費內存,如一個線程約佔用1MB堆棧空間)。並且CPU對線程的切換需要保存很多中間狀態、數據等,所以單個進程中的線程過多的話,性能反而會下降,CPU需要花費不少時間在各個線程之間來回切換,以致於耗費本該屬於程序運行的時間。

 

共享

由於一個進程中所有的線程可以獲取內存的共享數據,所以多個線程在同時訪問某個數據時,會出現數據異常、不一致的情況。這可能會使程序發生非常奇怪、難以發現的bug,而且這些bug難以重現和調試。例如,線程A要寫一塊數據,同時線程B也要寫這塊數據。此時就需要採取一定的技術手段,讓線程有先有後地去寫,而不能同時去寫,如果同時去寫,可能寫進去的數據就會出現互相覆蓋等數據不一致的錯誤。

 

執行

從編碼的角度來看,線程的執行彷彿是我們調用相應的函數來完成的,但實際上並非如此。我們調用線程執行的代碼,並不會在程序執行這段代碼時立即執行。準確的說,這段調用線程執行的代碼,僅僅是通知操作系統儘快地執行這個線程。線程具體的執行,是由操作系統,根據線程調度程序的分配機制決定的。


結語

本文的基本概念只能作爲多線程學習的一個開端,後續我將持續產出針對多線程應用的知識。想要將多線程技術更好的運用起來,可謂是,“路漫漫其修遠兮”。多線程的技術熟練運用,不光是高級開發人員與中級開發人員之間的一道分水嶺,它還是很多實際項目必須採用的一種技術方式。項目不單單隻滿足於功能而已,對於運行效率的提升,多線程技術的涉獵是不二法則。

戒驕戒躁,千萬不要急於求成,不要以爲多線程是一個很小的話題。多線程其實是一個很大的話題,請各位讀者要穩紮穩打,一步一個腳印地把多線程學好,這會終身受益。

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