線程的概念
1 什麽是線程
線程,有時被稱為輕量級進程,是程序執行的最小單元。一個標準的線程由線程ID、 程序計數器(pc)、一組寄存器和堆棧組成。通常,一個進程由多個線程組成,每個線程之間共享進程的內存空間(包括代碼段、數據段、堆等)及一些進程級的 資源(如打開的文件描述符和信號)。如下圖所示:
2 線程的訪問權限
線程的訪問非常自由,它可以訪問進程內存裏的所有數據,同時線程也擁有自己IDE私有存儲空間,包括以下幾方面:
1)棧
2)線程局部存儲(TLS)。
3)寄存器(包括PC寄存器)
3 線程調度和優先級
在單處理器對應多線程的情況下,並發是一種模擬出來的。操作系統通過讓多個線程輪流使用CPU,這樣每個線程就“看起來”在同時執行。
在線程調度中,線程至少有三種狀態,分別是:
1)運行:此時線程獲得CPU正在執行
2)就緒:此時線程只有獲得CPU就可以立刻執行
3)等待:此時線程正在等待某一事件發送,無法執行。
線程轉換圖:
4 Linux多線程
Linux對多線程的支持頗為貧乏,事實上,在Linux內核中並不存在真正意義上的線程概念。Linux將所有的執行實體(無論是線程還是進程)都稱為任務,每一個任務類似於一個單線程的進程,具有內存空間、執行實體、文件資源等。
fork函數產生一個和當前進程完全一樣的新進程,並和當前進程一樣從fork函數裏返回。
fork產生新任務的速度非常快,因為fork並不復制原任務的內存空間(這裏指 的是物理內存,父子進程的虛擬地址空間的獨立的),而是和原任務一起共享一個寫時復制(COW)的內存空間。所謂寫時復制,指的是兩個任務可以同時自由地 讀取內存,但任意一任務試圖對內存進行修改時,內存就會復制一份提供給修改方單獨使用,以免影響到其他的任務使用。
fork只能夠產生本任務的鏡像,如果要啟動新任務,則使用exec。exec可 以用新的可執行映像替換當前的可執行映像,因此在fork產生了一個新任務後,新任務可以exec來執行新的可執行文件。fork和exec都只用於產生 新任務,而如果要產生新線程,則可以使用clone。
5 線程安全
多線程程序處於一個多變的環境中,可訪問的全局變量和堆數據隨時都可能被其他的線程改變。因此多線程程序在並發時數據的一致性變得非常重要。
5.1 競爭和原子操作
多個線程同時訪問一個共享數據,可能造成錯誤的結果:
例如:
在許多體系結構上,++i的實現會如下:
1)讀取i到某個寄存器X
2)X++
3)將X的內存存儲回i
由於線程1和線程2的並發執行,因此兩個線程的執行序列可能如下:
從程序的邏輯看,正確的結果應該是i為0.但是由於執行的序列問題,可能出現的結果有0,1,2。可見,兩個線程同時操作一個共享數據會出現意想不到的結果。
很明顯,這裏出現錯誤的原因主要在於自增(++)操作被操作系統編譯為匯編代碼之 後不止一條指令,因此在多線程環境下就可能出現執行了一半而被調度系統打斷,去執行其他的代碼。如果單條指令是原子的,則執行就不會被打斷。問題是,盡管 原子操作非常方便,但是它僅適用於比較簡單的場合。
5.2 同步和鎖
為了避免多個線程同時讀寫一個數據而出現不可預料的結果,我們需要將各種線程對同一數據的訪問同步。所謂同步,即是指在一個線程訪問數據未結束的時候,其他線程不得對同一個數據進行訪問。
同步的最常見方法是加鎖。鎖是一種非強制機制,每一個線程在訪問數據或資源之前首先試圖獲取鎖,訪問完後釋放鎖。
二元信號量是最簡單的一種鎖,它只有兩種狀態:占用和非占用。它適合只能被唯一一個線程訪問的資源。
對於允許多個線程並發訪問的資源,使用多元信號量。一個初始值為N的信號量允許N個線程並發訪問。
互斥量和二元信號量類似。
臨界區是一段訪問臨界資源的代碼。臨界區和互斥量和信號量的區別在於,互斥量和信 號量在系統的任何進程都是可見的,也就是說,一個進程創建了一個互斥量或信號量,另一個進程試圖去獲取該鎖是合法的。然而,臨界區的作用僅限於同一進程內 的不同線程之間的同步,不能用於進程的同步。
讀寫鎖分為共享的和獨占的。
條件變量,使用條件變量可以讓許多線程一起等待某個事件的發生,當事件發生後,所有線程可以一起恢復。
6 可重入與線程安全
一個函數要成為可重入的,必須具有以下幾個特點:
1)不使用任何(局部)靜態或全局的非const變量
2)不返回任何(局部)靜態或全局的非const變量的指針
3)僅依賴於調用方提供的參數
4)不依賴於單個資源的鎖(mutex等)
5)不調用任何不可重入的函數
7 過度優化
有時候過度優化也會造成線程安全問題。
例如:
由於有鎖的保護,x++的行為不會被並發所破壞,那麽x似乎必然為2.然而,如果編譯器為了提高x的訪問速度,把x放入了某個寄存器中,那麽我們知道不同線程的寄存器是各自獨立的,此時就出現線程安全問題,例如:
可見,現在即使加鎖也不能保證結果正確。
我們可以使用volatile關鍵字試圖阻止過度優化。volatile可以阻止兩件事情:
1)阻止編譯器為了提高速度將一個變量緩存在寄存器內而不寫回。
2)阻止編譯器調整操作volatile變量的指令。
線程的概念