線程及線程安全
對于基于Linux操作系統的開發者來說,多線程是一個在開發和面試中不可避免的、被廣泛討論的話題。本文首先對線程進行簡單的介紹,然后介紹幾種保證線程安全的方法。
一、線程簡介
在介紹線程之前,要引入進程(Process)的概念。進程有狹義和廣義之分,狹義的進程是正在運行的程序的實例;廣義的進程是一個具有一定獨立功能的程序關于某個數據集合的一次運行活動,是操作系統動態執行的基本單元。
線程(Thread),有時被稱為輕量級進程(LWP),是程序執行流的最小單位;一個標準的線程由線程ID、當前指令指針(PC)、寄存器集合和堆棧組成。通常情況下,一個進程由一個到多個線程組成,各個線程之間共享程序的內存空間及一些進程級的資源。
在大多數軟件應用中,線程的數量都不止一個,多線程程序處在一個多變的環境中,可訪問的全局變量和堆數據隨時都可能被其他的線程改變,這就將“線程安全”的問題提上了議程。那么,如何確保線程的安全呢?
二、線程安全
一般說來,確保線程安全的方法有這幾個:競爭與原子操作、同步與鎖、可重入、過度優化。
1. 競爭與原子操作
多個線程同時訪問和修改一個數據,可能造成很嚴重的后果。出現嚴重后果的原因是很多操作被操作系統編譯為匯編代碼之后不止一條指令,因此在執行的時候可能執行了一半就被調度系統打斷了而去執行別的代碼了。一般將單指令的操作稱為原子的(Atomic),因為不管怎樣,單條指令的執行是不會被打斷的。
因此,為了避免出現多線程操作數據的出現異常,Linux系統提供了一些常用操作的原子指令,確保了線程的安全。但是,它們只適用于比較簡單的場合,在復雜的情況下就要選用其他的方法了。
2. 同步與鎖
為了避免多個線程同時讀寫一個數據而產生不可預料的后果,開發人員要將各個線程對同一個數據的訪問同步,也就是說,在一個線程訪問數據未結束的時候,其他線程不得對同一個數據進行訪問。
同步的最常用的方法是使用鎖(Lock),它是一種非強制機制,每個線程在訪問數據或資源之前首先試圖獲取鎖,并在訪問結束之后釋放鎖;在鎖已經被占用的時候試圖獲取鎖時,線程會等待,直到鎖重新可用。
二元信號量是最簡單的一種鎖,它只有兩種狀態:占用與非占用,它適合只能被***一個線程獨占訪問的資源。對于允許多個線程并發訪問的資源,要使用多元信號量(簡稱信號量)。
3. 可重入
一個函數被重入,表示這個函數沒有執行完成,但由于外部因素或內部因素,又一次進入該函數執行。一個函數稱為可重入的,表明該函數被重入之后不會產生任何不良后果。可重入是并發安全的強力保障,一個可重入的函數可以在多線程環境下放心使用。
4. 過度優化
在很多情況下,即使我們合理地使用了鎖,也不一定能夠保證線程安全,因此,我們可能對代碼進行過度的優化以確保線程安全。
我們可以使用volatile關鍵字試圖阻止過度優化,它可以做兩件事:***,阻止編譯器為了提高速度將一個變量緩存到寄存器而不寫回;第二,阻止編譯器調整操作volatile變量的指令順序。
在另一種情況下,CPU的亂序執行讓多線程安全保障的努力變得很困難,通常的解決辦法是調用CPU提供的一條常被稱作barrier的指令,它會阻止CPU將該指令之前的指令交換到barrier之后,反之亦然。
【本文是51CTO專欄作者“周兆熊”的原創文章,作者微信公眾號:周氏邏輯(logiczhou)】