程序員的自我修養(yǎng) Liunx多線程

Liunx的多線程

??Windows 對進程和線程的實現(xiàn)如同教科書一般標準,Windows內核有明確的線程和進程的概念。在Windows API中,可以使用明確的API: CreateProcess和 CreateThread來創(chuàng)建 進程和線程,并且有一系列的API來操縱它們。但對于Liunx來說,線程并不是一個通用的概念。

?Liunx內核中不存在真正意義上的線程概念。Liunx將所有的執(zhí)行實體(無論是線程還是進程)都稱為任務(Task),每一個任務概念上都類似于一個單線程的進程,具有內存空間、執(zhí)行實體、文件資源等。不過,Liunx下不同的任務之間可以選擇共享內存空間,因而在實際意義上,共享了同一個內存空間的多個任務構成了一個進程,這些任務也就成了這個進程里的線程。在Liunx下,用以下方法可以創(chuàng)建一個新的任務,如表1-2所示:

image.png

fork函數(shù)嘗試一個和當前進程完全一樣的新進程,并和當前進程一樣從fork函數(shù)里返回。

pid_t pid;
if (pid = fork())
{
.....
}

?在fork函數(shù)調用之后,新的任務將啟動并和本任務一起從fork函數(shù)返回。但不同的是本任務的fork將返回新任務pid,而新任務的fork將返回0.
?fork產生新任務的速度非???,因為fork并不復制原任務的內存空間,而是和原任務一起共享一個寫時復制(Copy on Write,COW)的內存空間(見圖1-10)。所謂寫時復制,指的是兩個任務可以同時自由地讀取內存,但任意一個任務試圖對內存進行修改時,內存就會復制一份提高內修改方單獨使用,以免影響到其他的任務使用。

image.png

?fork只能產生本任務的鏡像,因此必須要使用exec配合才能夠啟動別的新任務。exec可以用新的可執(zhí)行映像替換當前的可執(zhí)行映像。因此在fork產生了一個新任務只會,新任務可以調用exec來執(zhí)行新的可執(zhí)行文件。forkexec通常用于產生新任務,而如果要產生新線程,則可以用clone。clone函數(shù)原型如下:

int clone(int (*fn)(void *), void* child_stack,int flags, void* arg);

?使用clone可以產生一個新的任務,從指定的位置開始執(zhí)行,并且(可選的)共享當前進程的內存空間和文件等。如此就可以在實際效果上產生一個線程。

線程安全

??多線程程序處于一個多變的環(huán)境當中,可以訪問的全局變量和堆數(shù)據隨時都可能被其他的線程改變。因此多線程程序在并發(fā)時數(shù)據的一致性變得非常重要。

競爭與原子操作
??多個線程同手訪問一個共享數(shù)據,可能造成很惡劣的后果,下面是一個著名的例子,假設有兩個線程分別要執(zhí)行如表1-3所示的C代碼。

image.png

?在許多體系結構上,++i的實現(xiàn)方法會如下:

(1)讀取i到某個寄存器X
(2)X++
(3)將X的內容存儲回i
?由于線程1和線程2并發(fā)執(zhí)行,因此兩個線程的執(zhí)行序列很可能如下(注意,寄存器X的內容在不同的線程中是不一樣的,這里用X[1]和X[2]分別表示線程1和線程2中的X),如表1-4所示

image.png

?和明顯,自增(++)操作在多線程環(huán)境下會出現(xiàn)錯誤是因為這個操作被編譯為匯編代碼后不止一條指令,因此在執(zhí)行的時候可能執(zhí)行了一半就被調度系統(tǒng)打斷,去執(zhí)行別的代碼。我們把單指令的操作稱為原子的,因為無論如何,單條指令的執(zhí)行是不會被打斷的。為了避免出錯,很多體系結構都提供了一些常用操作的原子指令。

同步和鎖
?為了避免多個線程同時讀寫同一個數(shù)據而產生不可預料的后果,我們需要將各個線程對同一個數(shù)據的訪問同步(Synchronization)。所謂同步,既是指在一個線程訪問數(shù)據未結束的時候,其他線程不得對同一個數(shù)據進行訪問。如此,對數(shù)據的訪問被原子化了。
?同步的最常見方法是使用鎖(Lock)。鎖是一種非強制機制,每一個線程在訪問數(shù)據或資源之前首先試圖獲得(Acquire)鎖,并在訪問之后釋放(Release)鎖。在鎖已經被占用的時候試圖獲取鎖時,線程會等待,直到鎖重新可用。

二元信號量(Binary Semaphore)是最簡單的一種鎖,它只有兩種狀態(tài):占用與非占用。它適合只能被唯一一個線程獨占訪問的資源。當二元信號量處于非占用狀態(tài)時,第一個試圖獲取該二元信號量的線程會獲得該鎖,并將二元信號量置為占用狀態(tài),此后其他的所有試圖獲取該二元信號量的線程將會等待,直到該鎖被釋放。

多元信號量,允許多個線程并發(fā)訪問資源。一個初始值為N的信號量允許N個線程并發(fā)訪問。線程訪問資源的時候首先獲取信號量,進行如下操作:

  • 將信號量的值減1.
  • 如果信號量的值小于0,則進入等待狀態(tài),否則繼續(xù)執(zhí)行。
    訪問完資源之后,線程釋放信號量,進行如下操作:
  • 將信號量的值加1
  • 如果信號量的值小于1,喚醒一個等待中的線程。

互斥量(Mutex)與二元信號量類似,資源僅允許一個線程訪問,但和信號量不同的時,信號量在整個系統(tǒng)可以被任意線程獲取并釋放,也就是說,同一個信號量可以被系統(tǒng)中的一個線程獲取之后由另一個線程釋放。而互斥量則要求哪個線程獲取了互斥量,哪個線程就要負責釋放這個鎖,其他線程越俎代庖去釋放互斥量是無效的。

臨界區(qū)(Critical Section)是比互斥量更加嚴格的同步手段。在術語中,把臨界區(qū)的鎖的獲取稱為進入臨界區(qū),而把鎖的釋放稱為離開臨界區(qū)。臨界區(qū)和互斥量與信號量的區(qū)別在于,互斥量和信號量在系統(tǒng)的任何進程里都是可見的。也就是說,一個進程創(chuàng)建了一個互斥量或信號量,另一個進程試圖去獲取該鎖是合法的。然而,臨界區(qū)的作用范圍僅限于本進程,其他的進程無法獲取該鎖。除此之外,臨界區(qū)具有和互斥量相同的性質。

讀寫鎖(Read-Write Lock)。對于同一個鎖,讀寫鎖有兩種獲取方式。共享的(Shared)獨占的(Exclkusive)。當鎖處于自由狀態(tài)時,試圖以任何一種方式獲取鎖都能成功,并將鎖置于對應的狀態(tài)。如果鎖處于共享狀態(tài),其他線程以共享的方式獲取鎖仍然會成功,此時這個鎖分配給了多個線程。然而,如果其他線程試圖以獨占的方式獲取已經處于共享狀態(tài)的鎖,那么它就必須等待鎖被所有的線程釋放。相應的,處于獨占狀態(tài)的鎖將阻止任何其他線程獲取該鎖,不論它們試圖以哪種方式獲取。

image.png

條件變量(Condition Variable)作為一種同步手段,作用類似于一個柵欄。對于條件變量,線程可以用兩種操作,首先線程可以等待條件變量,一個條件變量可以被多個線程等待。其次,線程可以喚醒條件變量,此時某個或所有等待此條件變量的線程都會被喚醒并繼續(xù)支持。也就是說,使用條件變量可以讓許多線程一起等待某個事件的發(fā)生,當事件發(fā)生時(條件變量被喚醒),所有的線程可以一起恢復執(zhí)行。

可重入(Reentrant)與線程安全

?一個函數(shù)被重入,表示這個函數(shù)沒有執(zhí)行完成,由于外部因素或內部調用,又一次進入該函數(shù)執(zhí)行。一個函數(shù)要被重入,只有兩種情況:
(1)多個線程同時執(zhí)行這個函數(shù)
?(2) 函數(shù)自身(可能是經過多層調用之后)調用本身。

?一個函數(shù)被稱為可重入,表明函數(shù)被重入之后不會產生任何不良影響。一個函數(shù)要成為可重入的,必須具有如下幾個特點:

  • 不使用任務(局部)靜態(tài)或全局的非const變量。
  • 不返回任何(局部)靜態(tài)或全局的非const變量的指針。
  • 僅依賴于調用方提供的參數(shù)。
  • 不依賴任何單個資源的鎖(mutex等)。
  • 不調用任何不可重入函數(shù)。

可重入是并發(fā)安全的強力保障,一個可重入函數(shù)可以在多線程環(huán)境下放心使用。

多線程內部情況

三種線程模型
?線程的并發(fā)執(zhí)行時由多處理器或操作系統(tǒng)調度來實現(xiàn)的。但實際情況更會復雜一些:大多數(shù)操作系統(tǒng),包括Windows和Liunx,都在內核里提供線程的支持,內核線程(注:這里的內核線程和Liunx內核里的kernel_thread并比不是一回事)由多處理器或調度來實現(xiàn)并發(fā)。然而用戶實際使用的線程并不是內核線程,而是存在于用戶態(tài)的用戶線程。用戶線程并不一定在操作系統(tǒng)內核里對用同等數(shù)量的內核線程,例如某些輕量級的線程庫,對用戶來說如果有三個線程在同時執(zhí)行,對內核來說很可能只有一個線程。

用戶態(tài)多線程的三種實現(xiàn)方式:

1.一對一模型
?對應直接支持線程的系統(tǒng),一對一模型始終是最為簡單的模型。對一對一模型來說,一個用戶使用的線程就是唯一對應一個內核使用的線程(但反過來不一定,一個內核里的線程在用戶態(tài)不一定有對應的線程存在)

image.png

?這樣用戶線程就具有了和內核線程一致的優(yōu)點,線程之間的并發(fā)是真正的并發(fā),一個線程因為某原因阻塞時,其他線程執(zhí)行不會受到影響。此外,一對一模型也可以讓多線程程序在多處理器的系統(tǒng)上有更好的表現(xiàn)。

?在Liunx系統(tǒng)里使用clone產生的線程就是一個一對一線程,為此時在內核中有一個唯一的線程與之對應。


image.png

?一對一線程缺點有兩個:

  • 由于許多操作系統(tǒng)限制了內核線程的數(shù)量,因此一對一線程會讓用戶的線程數(shù)量受到限制;
  • 許多操作系統(tǒng)內核線程調度時,上下文切換的開銷較大,導致用戶線程的執(zhí)行效率下降。

2.多對一模型
?多對一模型將多個用戶線程映射到一個內核線程上,線程之間的切換由用戶態(tài)的代碼來進行,因此相對于一對一模型,多對一模型的線程切換要快速許多。

image.png

?多對一模型的缺點:

  • 如果其中一個用戶線程阻塞,所有線程都將無法執(zhí)行,因為此時內核里的線程也隨之阻塞了。
  • 多處理器系統(tǒng)上處理器的增多對多對一模型的線程性能也不會有明顯的幫組。
    ?優(yōu)點:高效的上下文切換和幾乎無限制的線程數(shù)量。

3.多對多模型
?多對多模型結合了多對一模型的一對一模型的特點,將多個用戶線程映射到少數(shù)但不止一個內核線程上

image.png

優(yōu)點:

  • 一個用戶線程阻塞不會所所有線程阻塞,因為此時還有別的線程可以被調度來執(zhí)行
  • 對用戶線程的數(shù)量沒什么限制
  • 在多處理器系統(tǒng)上,多對多模型的線程性能可以得到一定提升,不過提升幅度不如一對一模型高。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容