天天看點

第一章——線程的介紹

線程,有時被稱為輕量級程序,是程式執行的最小單元。一個标準的線程由線程ID、程式計數器(pc)、一組寄存器和堆棧組成。通常,一個程序由多個線程組成,每個線程之間共享程序的記憶體空間(包括代碼段、資料段、堆等)及一些程序級的資源(如打開的檔案描述符和信号)。如下圖所示:

第一章——線程的介紹

線程的通路非常自由,它可以通路程序記憶體裡的所有資料,同時線程也擁有自己IDE私有存儲空間,包括以下幾方面:

1)棧

2)線程局部存儲(TLS)。

3)寄存器(包括PC寄存器)

第一章——線程的介紹

在單處理器對應多線程的情況下,并發是一種模拟出來的。作業系統通過讓多個線程輪流使用CPU,這樣每個線程就“看起來”在同時執行。

線上程排程中,線程至少有三種狀态,分别是:

1)運作:此時線程獲得CPU正在執行

2)就緒:此時線程隻有獲得CPU就可以立刻執行

3)等待:此時線程正在等待某一事件發送,無法執行。

線程轉換圖:

第一章——線程的介紹

Linux對多線程的支援頗為貧乏,事實上,在Linux核心中并不存在真正意義上的線程概念。Linux将所有的執行實體(無論是線程還是程序)都稱為任務,每一個任務類似于一個單線程的程序,具有記憶體空間、執行實體、檔案資源等。

第一章——線程的介紹

fork函數産生一個和目前程序完全一樣的新程序,并和目前程序一樣從fork函數裡傳回。

fork産生新任務的速度非常快,因為fork并不複制原任務的記憶體空間(這裡指的是實體記憶體,父子程序的虛拟位址空間的獨立的),而是和原任務一起共享一個寫時複制(COW)的記憶體空間。所謂寫時複制,指的是兩個任務可以同時自由地讀取記憶體,但任意一任務試圖對記憶體進行修改時,記憶體就會複制一份提供給修改方單獨使用,以免影響到其他的任務使用。

fork隻能夠産生本任務的鏡像,如果要啟動新任務,則使用exec。exec可以用新的可執行映像替換目前的可執行映像,是以在fork産生了一個新任務後,新任務可以exec來執行新的可執行檔案。fork和exec都隻用于産生新任務,而如果要産生新線程,則可以使用clone。

多線程程式處于一個多變的環境中,可通路的全局變量和堆資料随時都可能被其他的線程改變。是以多線程程式在并發時資料的一緻性變得非常重要。

5.1 競争和原子操作

多個線程同時通路一個共享資料,可能造成錯誤的結果:

例如:

第一章——線程的介紹

在許多體系結構上,++i的實作會如下:

1)讀取i到某個寄存器X

2)X++

3)将X的記憶體存儲回i

由于線程1和線程2的并發執行,是以兩個線程的執行序列可能如下:

第一章——線程的介紹

從程式的邏輯看,正确的結果應該是i為0.但是由于執行的序列問題,可能出現的結果有0,1,2。可見,兩個線程同時操作一個共享資料會出現意想不到的結果。

很明顯,這裡出現錯誤的原因主要在于自增(++)操作被作業系統編譯為彙編代碼之後不止一條指令,是以在多線程環境下就可能出現執行了一半而被排程系統打斷,去執行其他的代碼。如果單條指令是原子的,則執行就不會被打斷。問題是,盡管原子操作非常友善,但是它僅适用于比較簡單的場合。

5.2 同步和鎖

為了避免多個線程同時讀寫一個資料而出現不可預料的結果,我們需要将各種線程對同一資料的通路同步。所謂同步,即是指在一個線程通路資料未結束的時候,其他線程不得對同一個資料進行通路。

同步的最常見方法是加鎖。鎖是一種非強制機制,每一個線程在通路資料或資源之前首先試圖擷取鎖,通路完後釋放鎖。

二進制信号量是最簡單的一種鎖,它隻有兩種狀态:占用和非占用。它适合隻能被唯一一個線程通路的資源。

對于允許多個線程并發通路的資源,使用多元信号量。一個初始值為N的信号量允許N個線程并發通路。

互斥量和二進制信号量類似。

臨界區是一段通路臨界資源的代碼。臨界區和互斥量和信号量的差別在于,互斥量和信号量在系統的任何程序都是可見的,也就是說,一個程序建立了一個互斥量或信号量,另一個程序試圖去擷取該鎖是合法的。然而,臨界區的作用僅限于同一程序内的不同線程之間的同步,不能用于程序的同步。

讀寫鎖分為共享的和獨占的。

第一章——線程的介紹

條件變量,使用條件變量可以讓許多線程一起等待某個事件的發生,當事件發生後,所有線程可以一起恢複。

一個函數要成為可重入的,必須具有以下幾個特點:

1)不使用任何(局部)靜态或全局的非const變量

2)不傳回任何(局部)靜态或全局的非const變量的指針

3)僅依賴于調用方提供的參數

4)不依賴于單個資源的鎖(mutex等)

5)不調用任何不可重入的函數

有時候過度優化也會造成線程安全問題。

第一章——線程的介紹

由于有鎖的保護,x++的行為不會被并發所破壞,那麼x似乎必然為2.然而,如果編譯器為了提高x的通路速度,把x放入了某個寄存器中,那麼我們知道不同線程的寄存器是各自獨立的,此時就出現線程安全問題,例如:

第一章——線程的介紹

可見,現在即使加鎖也不能保證結果正确。

我們可以使用volatile關鍵字試圖阻止過度優化。volatile可以阻止兩件事情:

1)阻止編譯器為了提高速度将一個變量緩存在寄存器内而不寫回。

2)阻止編譯器調整操作volatile變量的指令。

繼續閱讀