上一篇:線程概念與控制https://blog.csdn.net/Small_entreprene/article/details/146704881?sharetype=blogdetail&sharerId=146704881&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link我們學習了線程的控制及其相關概念之后,我們清楚:線程是共享地址空間的,所以線程會共享大部分資源。對于多線程來說,訪問的共享資源稱為共功資源,而多執行流訪問公共資源的時候,可能會造成多種情況的數據不一致問題,因為公共資源并沒有加保護,為了解決這樣的問題,我們就下來就要學習同步與互斥:
互斥話題
在當前學習進程間通信中的信號量的時候,我們有談及,現在我們來快速看看什么是互斥,互斥的相關概念:
進程與線程(執行流)互斥機制的基本概念:
-
臨界資源:指在多線程執行過程中,被多個線程共享的資源。(可以理解為被保護起來的共享資源)
-
臨界區:指線程內部用于訪問臨界資源的代碼段。
-
互斥:確保在任何給定時刻,只有一個執行線程能夠進入臨界區,從而對臨界資源進行訪問,這通常用于保護臨界資源。
-
原子性:指一個操作在執行過程中不會被任何調度機制中斷,該操作要么完全執行,要么完全不執行。(后續將討論如何實現原子性)
看一個現象
我們下面來見見一種現象(除了多執行流往顯示器文件上打印這個搶占臨界資源的現象外,另外一種數據不一致問題),然后快速的使用鎖(pthread鎖/互斥鎖)來進行解決一下:
樣例代碼:簡單的模擬一下搶票過程(多線程進行搶票)
代碼是一個簡單的多線程售票系統的示例,其中包含一個共享變量 ticket
,表示剩余的票數。代碼中創建了四個線程,每個線程都試圖通過調用 route
函數來銷售車票。然而,由于多個線程同時訪問和修改 ticket
變量,這會導致競態條件(race condition),從而使得程序的行為不可預測。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 100; // 初始化票數為100void *route(void *arg)
{char *id = (char*)arg; // 從線程參數中獲取線程IDwhile ( 1 ) // 無限循環,直到票賣完{if ( ticket > 0 ) // 如果還有票{usleep(1000); // 模擬售票操作的延時printf("%s sells ticket:%d\n", id, ticket); // 打印售票信息ticket--; // 票數減一} else {break; // 如果票賣完了,退出循環}}return nullptr; // 線程結束
}int main( void )
{pthread_t t1, t2, t3, t4; // 定義四個線程的變量pthread_create(&t1, NULL, route, (void*)"thread 1"); // 創建線程1pthread_create(&t2, NULL, route, (void*)"thread 2"); // 創建線程2pthread_create(&t3, NULL, route, (void*)"thread 3"); // 創建線程3pthread_create(&t4, NULL, route, (void*)"thread 4"); // 創建線程4pthread_join(t1, NULL); // 等待線程1結束pthread_join(t2, NULL); // 等待線程2結束pthread_join(t3, NULL); // 等待線程3結束pthread_join(t4, NULL); // 等待線程4結束return 0; // 程序結束
}
票數竟然是負數!!!?
解決這個問題
由于 ticket
是一個共享變量,且在 routine
函數中沒有適當的同步機制來保護對它的訪問,因此當多個線程同時執行 ticket--
操作時,可能會出現以下問題:
-
票數不準確:可能會售出超過100張的票,因為多個線程可能同時讀取相同的
ticket
值,然后各自減一。 -
數據競爭:多個線程同時寫入
ticket
變量,導致最終的票數不正確。
為了解決這個現象,可以使用互斥鎖(mutex)來同步對 ticket
變量的訪問。以下是修改后的代碼示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 100; // 初始化票數為100
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 初始化互斥鎖void *route(void *arg)
{char *id = (char *)arg; // 從線程參數中獲取線程IDwhile (1) // 無限循環,直到票賣完{pthread_mutex_lock(&lock); // 加鎖if (ticket > 0) // 如果還有票{usleep(1000); // 模擬售票操作的延時printf("%s sells ticket:%d\n", id, ticket); // 打印售票信息ticket--; // 票數減一}pthread_mutex_unlock(&lock); // 解鎖if (ticket <= 0){break; // 如果票賣完了,退出循環}}return NULL; // 線程結束
}int main(void)
{pthread_t t1, t2, t3, t4; // 定義四個線程的變量pthread_create(&t1, NULL, route, (void *)"thread 1"); // 創建線程1pthread_create(&t2, NULL, route, (void *)"thread 2"); // 創建線程2pthread_create(&t3, NULL, route, (void *)"thread 3"); // 創建線程3pthread_create(&t4, NULL, route, (void *)"thread 4"); // 創建線程4pthread_join(t1, NULL); // 等待線程1結束pthread_join(t2, NULL); // 等待線程2結束pthread_join(t3, NULL); // 等待線程3結束pthread_join(t4, NULL); // 等待線程4結束pthread_mutex_destroy(&lock); // 銷毀互斥鎖return 0; // 程序結束
}
?
理解為什么會數據不一致&&認識加鎖的接口
我們先來理解一下這數據為什么會不一致!?
為什么票數會減到負數?
?ticket --和 if ( ticket > 0)判斷是導致數據不一致的主要影響。(--不是主要矛盾,但是和它確實有關)
由于 ticket--
不是原子的(要么減,要么不減),ticket這個變量是在內存當中的,在計算機當中,對一個變量作 -- 其實本身屬于算數運算,而在計算機馮諾依曼體系當中,目前我們知道,我們對變量作 -- 操作只能由CPU來進行計算,也就是說在我們所對應的整個計算機當中,只有CPU能夠對這個ticket作 -- ,這時候:(簡單理解,最主要是三步)
- 第一步:需要將ticket讀入到CPU當中(從內存到CPU,嚴格來說是導入到CPU的某些寄存器,比如說ebx)(內存本身沒有計算能力)。
- 第二步:CPU要進行對該寄存器里的值作減操作。
- 第三步:由于?-- 操作是會影響原始的值的,并不是作減法操作,所以需要將減完之后的值寫回內存,寫回內存要來保證對原始值作更改(100->99)。
其實CPU對ticket的一系列操作,宏觀上就是cpu正在執行一個進程或線程的代碼,在做進程,線程調度(ticket--),所以從執行流的調度上來講,CPU會存在當前執行流的上下文數據(防止臨時變量因為執行流的切換造成數據的丟失)—————上面作為背景知識,下面來解釋說為什么ticket--是原子性的:
CPU內,除了數據類的寄存器,還有pc指針,程序計數器等等,假設pc指針當前指向的是0xff00,代表正在執行ticket--,我們上面的三步就可以翻譯成匯編指令:(大概)
0XFF00 mov ebx ticket
0XFF02 減少 ebx 1
0XFF04 mov 寫回 ticket所對應的地址 ebx
如圖:
在正準備執行第三條語句的時候,該進程發生了切換,線程切換,就需要保存上下文數據,保存了 ebp: 99??,pc指針: 0XFF04 (正準備執行第三條語句)?在執行第三條語句的時候,線程被切換了,該上下文就被放入到系統的等待隊列了:
此時CPU內的所有寄存器的數據已經變相的廢棄了,因為可以被覆蓋了,CPU就繼續選擇一個線程B進行調度(上面鏈入等待隊列的我們稱為線程A),線程B也有自己的代碼和數據,但是連兩個線程是執行的相同代碼,所以執行地址沒有發生改變,將此時CPU寄存器內的值進行覆蓋(不需要害怕覆蓋造成數據的丟失,因為線程A上下文數據是被保存起來了),線程B照常執行,沒有其他線程影響,執行完后,就將100減減之后的99,寫回內存了:
線程B就完整的完成了一次ticket--,假設線程B運氣很好,一次就將ticket的值減到1,準備將ticket的值減為0時,此時pc指針指向0XFF00,線程B正準備執行0XFF00的時候,線程B被切換了,吸納線程B就要保存自己的上下文數據:
線程A調回來了,但是CPU首先不是調度線程A,而是對線程A進行恢復上下文:
回復完后執行的是第三步,這就將寄存器中的99寫回到內存,這就導致線程B之前的工作全部白做了!!!這就造成了數據不一致問題!我們這個例子為的是解釋ticket--操作不是原子的。
我想說的是:一個整數的全局變量,并不是原子的!!! (因為C語言的--操作是會被轉化成3條匯編,三條匯編之間的任意一個位置,線程都可能被中斷切換,然后換入下一個線程;又因為線程資源是共享的,所以對應的ticket--操作并不是--的)(這也是為什么我們之前說信號量本質就是一個計數器,但是我們不敢用一個整數的全局變量來進行++/--,因為其操作不是原子的)
所以,簡單來說:當前,一種對原子性極簡式的理解是,當前來看,一條匯編語句,就是原子的!!!?
但是,為什么票數被減到了負數?
其實是該語句:if(ticket > 0) 是主要矛盾!!!對ticket是否大于0進行判斷,其實本質是一種計算,這種計算,我們稱之為邏輯計算,我們得到的是bool值,而且所有線程都是需要對其進行判斷的,假設ticket被安全的減到1,此時線程1將ticket載入到寄存器當中,準備要進行邏輯運算,我們可以將其操作看成兩步:
- 載入
- 判斷
但是在執行載入之后,還沒有執行下一條匯編語句的時候,線程1被線程2切換走了,線程2將ticket載入了,也是和線程1遭遇一樣,被切走了,依次,切到線程4,也是在第一步后被切走,之后,線程1,2,3,4就按照順序喚醒:
- 線程1執行--,就將其:1--->0;
- 線程2執行--,就將其:0--->-1;
- 線程3執行--,就將其:-1--->-2;
- 線程4執行--,就將其:-2--->-3;
判斷也是訪問共享資源!!!其中usleep也是為了堆積線程,然后才能使數據不一致現象更具直觀性!
綜上:一個全局資源沒有加保護,可能在多線程執行的時候,發生并發問題。我們將多線程導致的并發問題,比如說上面的搶票問題,我們稱之為線程安全問題!!!?
該routine函數也是被多執行流進入了,因為函數內部又全局資源,所以該函數是不可重入函數。
其實為了讓我們該搶票搶到負數,usleep后續輔助之外,重要的是,在多線程中,要制造更多的并發,更多的切換,我們并發的話,是創建了4個線程,那么,我們來好好談談這“更多的切換”
我們知道,線程切換其實就是對當前線程的上下文數據,線程上下文切換通常由以下幾種情況觸發:
-
時間片到期:操作系統為每個線程分配一個時間片(Time Quantum),當線程運行的時間達到分配的時間片時,操作系統會強制切換到其他線程。
-
線程阻塞:線程在等待某些資源(如 I/O 操作、鎖等)時會進入阻塞狀態,操作系統會切換到其他就緒的線程。
-
線程優先級調整:操作系統根據線程的優先級動態調整線程的調度順序,高優先級的線程可能會中斷低優先級的線程。
-
線程主動讓出 CPU:線程可以通過調用某些系統調用(如
yield
)主動讓出 CPU,操作系統會切換到其他線程。
當線程調用 usleep
函數時,它會通過系統調用陷入內核態。內核記錄線程的暫停時間,并將其置為等待狀態。時間到期后,線程被喚醒并準備返回用戶態。此時,內核會恢復線程的上下文信息,檢查線程狀態和資源使用情況,確保一切正常后,通過特定指令將控制權安全地交還給用戶態線程,使其從暫停處繼續執行。
所以解決數據不一致問題,我們就需要引入鎖得概念:
pthread庫為我們用戶提供線程得相關接口,線程在并發訪問的時候,全局資源那么多,線程就注定要為我們用戶提供各種各樣得鎖,我們所要使用到得鎖:pthread_mutex_t:
pthread_mutex_t
是 POSIX 線程庫(pthread)中用于實現互斥鎖(Mutex)的數據類型,主要用于在多線程程序中保護共享資源,防止多個線程同時訪問導致數據競爭和不一致問題。
互斥鎖是一種同步機制,用于確保一次只有一個線程可以訪問共享資源。當一個線程獲取了互斥鎖后,其他線程必須等待,直到該線程釋放鎖。
pthread_mutex_t
是一個不透明的數據類型,其具體實現由線程庫提供。通常,它是通過結構體或其他數據結構來實現的,但用戶不需要直接操作其內部細節。
互斥鎖可以通過靜態初始化或動態初始化來創建。
靜態初始化
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
這種方式適用于全局或靜態變量的互斥鎖。一旦是這么定義得,那么該鎖不需要被釋放!!!該鎖會在程序執行結束之后,自動釋放
動態初始化
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
這種方式適用于動態分配的互斥鎖。pthread_mutex_init
函數的第二個參數是一個指向 pthread_mutexattr_t
的指針,用于設置互斥鎖的屬性。如果傳入 NULL
,則使用默認屬性。之后不使用了,需要對對應的鎖進行釋放!!!
其實歸根結底,所有得鎖都要被所有進程看到的,不管是以參數的形式,假如在main函數中定義,然后傳給所有線程,還是說直接定義全局得鎖,因為鎖要保護我們的代碼,所有線程在訪問臨界資源之前,必須先申請鎖,申請鎖就需要先看到鎖。
怎么申請鎖:pthread_mutex_lock(&lock);
加鎖(屬于申請鎖的阻塞版本)
pthread_mutex_lock(&lock);
所有線程競爭鎖,如果鎖已經被其他線程占用,調用線程將阻塞掛起,直到鎖被釋放。
不過,多線程競爭申請鎖,多線程都得先看到鎖,所本身就是臨界資源,鎖是來保護共享資源的,那么誰來保護鎖的安全呢?
所以,pthread_mutex_lock(&lock);這個動作要求是要具有原子性的!!!??
加鎖成功,線程就繼續向后運行,訪問臨界區代碼,訪問臨界資源;加鎖失敗,線程就會阻塞掛起,所以鎖提供的能力的本質:執行臨界區代碼由并行轉化成串行。?
注意:加鎖:盡量加鎖的范圍粒度要比較細,盡可能的不要包含太多的非臨界區代碼。
嘗試加鎖(屬于申請鎖的非阻塞版本)
int ret = pthread_mutex_trylock(&lock);
if (ret == 0) {// 鎖獲取成功
} else if (ret == EBUSY) {// 鎖已被占用,獲取失敗
}
pthread_mutex_trylock
會嘗試獲取鎖,但如果鎖已經被占用,它不會阻塞,而是立即返回 EBUSY
。
解鎖
pthread_mutex_unlock(&lock);
釋放鎖,允許其他線程獲取鎖。
?當互斥鎖不再使用時,可以通過以下函數銷毀:
pthread_mutex_destroy(&lock);
銷毀互斥鎖后,其占用的資源將被釋放,但互斥鎖不能再被使用。
所以,我們對于來簡單的來豐富一下解決方案:(此時還沒有進行加鎖和解鎖)
int ticket = 1000;class ThreadData
{
public:ThreadData(const std::string &n, pthread_mutex_t &lock): name(n),lockp(&lock){}~ThreadData() {}std::string name;pthread_mutex_t *lockp;
};// 加鎖:盡量加鎖的范圍粒度要比較細,盡可能的不要包含太多的非臨界區代碼
void *route(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);while (1){if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}else{break;}}return nullptr;
}int main(void)
{pthread_mutex_t lock;pthread_mutex_init(&lock, nullptr); // 初始化鎖pthread_t t1, t2, t3, t4;ThreadData *td1 = new ThreadData("thread 1", lock);pthread_create(&t1, NULL, route, td1);ThreadData *td2 = new ThreadData("thread 2", lock);pthread_create(&t2, NULL, route, td2);ThreadData *td3 = new ThreadData("thread 3", lock);pthread_create(&t3, NULL, route, td3);ThreadData *td4 = new ThreadData("thread 4", lock);pthread_create(&t4, NULL, route, td4);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_mutex_destroy(&lock);//鎖用完了,就要釋放鎖!!!:我們是動態初始化的鎖(局部的鎖)return 0;
}
因為加鎖是要盡量細化加鎖的范圍粒度,盡可能的不要包含太多的非臨界區代碼,所以,因為在routine函數中,while語句當中的if語句都是臨界資源,所以,我們需要在if前進行加鎖。不過,不能在while之前進行加鎖,不然就會導致一個線程獨自將ticket--為0了:
//routine函數中進行加鎖(對共享資源進行保護: 共享資源--->臨界資源)while (1){pthread_mutex_lock(td->lockp);//進行加鎖if (ticket > 0)//.....}
?加鎖完成之后,我們需要進行解鎖,以實現其他線程獲取鎖。但是如下的解鎖位置是錯誤的:
// 加鎖:盡量加鎖的范圍粒度要比較細,盡可能的不要包含太多的非臨界區代碼
void *route(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);while (1){// LockGuard guard(*td->lockp); // 加鎖完成, RAII風格的互斥鎖的實現pthread_mutex_lock(td->lockp);//進行加鎖if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}//錯誤的解鎖位置pthread_mutex_unlock(td->lockp);else{break;}}return nullptr;
}
邏輯錯誤:解鎖位置可能導致死鎖
在代碼中,pthread_mutex_unlock(td->lockp);
被放置在了 if (ticket > 0)
的代碼塊之外,但與 else
語句同級。這會導致以下問題:
-
如果
ticket > 0
,線程會執行pthread_mutex_unlock(td->lockp);
,這是正常的解鎖操作。 -
但如果
ticket <= 0
,線程會進入else
分支并執行break
,此時 線程會直接退出循環,而沒有執行解鎖操作。 -
結果:如果線程在
ticket <= 0
時退出循環,它會持有鎖但沒有釋放鎖,導致其他線程無法獲取鎖,從而引發死鎖。
代碼風格問題:解鎖位置不清晰
-
解鎖操作應該與加鎖操作對稱,即在加鎖的代碼塊結束時進行解鎖。在當前代碼中,解鎖操作被放置在了錯誤的位置,導致代碼邏輯不清晰,容易引發錯誤。
-
正確的做法是將解鎖操作放在加鎖代碼塊的末尾,確保無論是否進入
if
或else
分支,鎖都能被正確釋放。(當然也可以在if和else分支里都進行解鎖,但是這樣的話就會有點代碼冗余)
下面是修正后的代碼:
void *route(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);while (1){pthread_mutex_lock(td->lockp); // 加鎖if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}else{break;}pthread_mutex_unlock(td->lockp); // 正確的解鎖位置}return nullptr;
}
我們運行這個代碼:?
所以,至此我們就完成了對共享資源的保護了!!!?(上面我們使用的是局部的鎖,也就是動態初始化的鎖,我們也可以使用全局的鎖,對應的就對全局的鎖進行解鎖了,而且不需要對其全局的鎖進行銷毀)
互斥鎖的屬性可以通過 pthread_mutexattr_t
來設置。例如:
-
互斥鎖類型:可以設置為普通鎖(
PTHREAD_MUTEX_NORMAL
)、遞歸鎖(PTHREAD_MUTEX_RECURSIVE
)、錯誤檢查鎖(PTHREAD_MUTEX_ERRORCHECK
)等。 - 共享屬性:可以設置為進程內共享(
PTHREAD_PROCESS_PRIVATE
)或進程間共享(PTHREAD_PROCESS_SHARED
)。
- 死鎖:如果線程獲取鎖后沒有釋放,或者多個線程以不同的順序獲取多個鎖,可能會導致死鎖。需要謹慎設計鎖的使用邏輯。錯誤的解鎖位置可能導致線程在某些情況下沒有釋放鎖,從而引發死鎖。
-
遞歸鎖:如果需要在同一個線程中多次獲取同一個鎖,可以使用遞歸鎖(
PTHREAD_MUTEX_RECURSIVE
)。 -
線程安全:互斥鎖是線程安全的,但需要正確使用,否則可能導致數據競爭或死鎖。
理解鎖
鎖被看到,意味著鎖也是全局的,但是進程之間想要看到的話就很難,不過線程間想要看到是很容易的,因為線程資源是共享的。但是進程間互斥之前沒有講過,之說了線程間互斥,那么兩個進程之間該如何實現互斥呢?
假設有兩個進程,創建出來一個共享內存,鎖(pthread_mutex_t)的本質就是一個變量,一個空間,我們直接將共享內存的其實地址shm,進行強制類型轉化,即 (pthread_mutex_t*)shm; 我們不就可以直接使用pthread_mutex_init(); 進行初始化,使用 pthread_mutex_destroy(); 進行銷毀,剩下的就可以通過鎖的機制,實現進程間互斥了。
不過我們會避免多進程進行通信(以共享內存的方式,鎖只是實現進程間互斥的解決辦法),后面學習網絡之后,自然知道是為什么了。
加鎖之后,在臨界區內部,允許線程切換嗎?切換了會怎么樣?
不管是臨界區還是非臨界區,在操作系統看來,終歸還是代碼,所以都是允許隨時切換的,那么我們在代碼中ticket--,不會還會出現我們上面討論的問題了嗎?
舉個例子:
超級自習室:是一個只允許一個人的自習室。
超級自習室旁邊的墻上:只有一把鑰匙。
我:線程。
每次超級自習室都要好多人惦記,不過,今天我來得比較早,所以我將墻上的鑰匙(獲取鎖成功)拿到了,我就進入到超級自習室里做我在超級自習室里面該做的事情(執行臨界區代碼),但是超級自習室只容許一個人,所以,我就將其鎖住了,其他人并進不來,因為他們沒有鑰匙,我在里里面呆了一段時間,想去上廁所,因此我出來將其門鎖上(線程切換:所以線程是可以切換的),因為其他人并沒有我這一把唯一可以進入到超級自習室的鎖,所以其他人是進不來的。
經過上面的簡單的生活例子,我們知道,其實線程切換并不影響,并不會引發上面沒有加鎖的數據不一致問題,因為當前線程并沒有釋放鎖,當前線程是持有鎖進行切換的,即便被切換走了,其他線程也需要等待該線程回來執行完代碼,然后釋放鎖,其他線程才能夠展開對鎖的競爭,進入臨界區!!!
所以站在外面的人是怎么看待我的自習過程呢?
站在外面人的角度:要么不用這個超級自習室,要么用完這個超級自習室,對外面的人才有意義。 (這就是原子性的特性,我們可以理解為:我的自習,對外面的人,是原子的!!!)(我們上面是將原子性簡單理解為一條匯編語句,知道了鎖后,我們可以理解說,我們可以將一個臨界區進行原子化!!!)
有了上面的過度,我們接下來,來真正理解一下,什么才是鎖。
鎖的原理
鎖的原理是通過一系列機制來確保對共享資源的訪問是安全的,避免多個線程或進程同時修改共享資源導致的數據不一致問題。鎖的核心目標是互斥(Mutual Exclusion)和同步(Synchronization)。
鎖不一定是互斥鎖。鎖是一個更廣泛的概念,互斥鎖(Mutex)只是其中一種常見的類型。我們下面對于互斥目標,主要圍繞互斥鎖來進行理解。
硬件級實現(只有內核在用):關閉時鐘中斷
對于一個代碼塊(不是一條匯編),這代碼塊可以隨時被切換,切換是因為時間片到了,操作系統會一直調度線程,一直在做中斷,一直檢測線程的時間片,一旦切換,代碼就發生交叉了,所以鎖的實現的硬件級有一個最簡單粗暴的做法:關閉時鐘中斷。
關閉時鐘中斷的原理
關閉時鐘中斷的基本思想是:
-
關閉中斷:在進入臨界區之前,關閉時鐘中斷,這樣當前線程不會被搶占,從而確保臨界區代碼不會被其他線程中斷。
-
執行臨界區代碼:在沒有中斷的情況下,安全地執行臨界區代碼。
-
打開中斷:臨界區代碼執行完畢后,重新打開中斷,恢復正常的線程調度。
這種方法的優點是簡單直接,但缺點也非常明顯:
-
風險高:如果臨界區代碼執行時間過長,或者發生死循環,會導致系統無法響應中斷,從而導致系統死機。
-
僅適用于單核處理器:在多核處理器中,關閉中斷無法阻止其他核心上的線程訪問共享資源。
現代操作系統中的鎖實現
現代操作系統和多核處理器環境中,鎖的實現主要依賴于硬件級的原子操作(如 compare-and-swap
和 test-and-set
),而不是關閉中斷。這些原子操作由處理器直接支持,確保在多核環境中,對共享變量的操作是原子的。
軟件實現(大多使用的鎖,并不簡單粗暴,使用交換)
為了實現互斥鎖操作,大多數體系結構都提供了 swap
或 exchange
指令。該指令的作用是把寄存器和內存單元的數據相交換。由于只有一條指令,保證了原子性。即使是多處理器平臺,訪問內存的總線周期也有先后。一個處理器上的交換指令執行時,另一個處理器的交換指令只能等待總線周期。
下面就是pthread_mutex_lock和pthread_mutex_unlock的偽代碼:
其實鎖就是一種標記位,在內存當中也是需要開辟空間的,我們可以將其鎖暫時看成是一個整數:為1,表示該鎖是沒有任何線程持有的:
接下來,肯定會有很多線程來申請鎖,我們就假設為兩個線程:線程A和線程B。
我們發現,申請鎖的操作都和%al這個寄存器有關(將其置0,在放入當前mutex的值)。進程或者線程切換時,CPU內的寄存器硬件只有一套,但CPU寄存器內的數據可以有多份,各自的一份稱為當前執行流的上下文。(切換時,自己打包帶走)
換句話說,如果把一個變量的內容,交換到CPU寄存器內部,本質就是把該變量的內容獲取到當前執行流的硬件上下文中,再本質一點就是當前CPU寄存器硬件上下文(其實就是各個寄存器的內容)屬于進程/線程私有的!!!
所以我們使用swap/exchange將內存中的變量交換到CPU的寄存器中,本質時當前線程/進程在獲取鎖,因為是交換,不是拷貝!!!所以mutex當中的1,只有一份!!!所以誰申請,誰就持有鎖!!!
線程A和線程B申請鎖,進來 movb $0, %al ,可能A進來到這就被切走了,但是沒有關系,因為該步驟是清理自己的數據的,彼此不會互相影響。
A執行到xchgb %al, mutex 后,可能被切換,線程A被切換走,會帶走:%a: 1?,當線程B要交換時,就是拿mutex中的0換0,因為線程A將1帶走了(這就是交換的效果,不是拷貝)。這就是申請鎖!!!
unlock就很簡單了,只需要將mutex的內容置1(movb $1, mutex),這就可以保證mutex有1可以被其他線程交換拿到了。這就是解鎖!!!
我們ticket--不是原子性的就是因為是拷貝,不是交換!!!
上面就是互斥鎖,mutex的原理!!!
C++11其實也是為我們用戶提供了: std::mutex(頭文件:#include<mutex>)就是封裝了鎖。
那么我們現在來自己封裝一個互斥量,面向對象化:
互斥量的封裝
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>namespace MutexModule
{// 自定義互斥鎖類,封裝了 pthread_mutex_t 的操作class Mutex{public:// 構造函數:初始化互斥鎖Mutex(){// 使用 pthread_mutex_init 初始化互斥鎖// nullptr 表示使用默認的互斥鎖屬性pthread_mutex_init(&_mutex, nullptr);}// 加鎖操作void Lock(){// 調用 pthread_mutex_lock 嘗試加鎖// 如果鎖已被其他線程持有,當前線程將阻塞int n = pthread_mutex_lock(&_mutex);(void)n; // 忽略返回值,實際使用中應檢查返回值處理錯誤}// 解鎖操作void Unlock(){// 調用 pthread_mutex_unlock 釋放鎖// 只有持有鎖的線程可以解鎖int n = pthread_mutex_unlock(&_mutex);(void)n; // 忽略返回值,實際使用中應檢查返回值處理錯誤}// 析構函數:銷毀互斥鎖~Mutex(){// 在對象銷毀時,釋放互斥鎖資源pthread_mutex_destroy(&_mutex);}private:pthread_mutex_t _mutex; // 內部使用的 pthread 互斥鎖};// RAII 風格的鎖管理類,確保互斥鎖在作用域結束時自動釋放class LockGuard{public:// 構造函數:自動加鎖LockGuard(Mutex &mutex) : _mutex(mutex){// 在構造時調用 Mutex 的 Lock 方法加鎖_mutex.Lock();}// 析構函數:自動解鎖~LockGuard(){// 在作用域結束時,自動調用 Mutex 的 Unlock 方法釋放鎖_mutex.Unlock();}private:Mutex &_mutex; // 引用一個 Mutex 對象};
}
其中:?
// RAII 風格的鎖管理類,確保互斥鎖在作用域結束時自動釋放class LockGuard{public:// 構造函數:自動加鎖LockGuard(Mutex &mutex) : _mutex(mutex){// 在構造時調用 Mutex 的 Lock 方法加鎖_mutex.Lock();}// 析構函數:自動解鎖~LockGuard(){// 在作用域結束時,自動調用 Mutex 的 Unlock 方法釋放鎖_mutex.Unlock();}private:Mutex &_mutex; // 引用一個 Mutex 對象};
LockGuard
類通過 RAII(資源獲取即初始化)風格管理鎖,確保互斥鎖在作用域開始時自動加鎖,并在作用域結束時自動解鎖,從而有效避免因忘記解鎖導致的死鎖問題,簡化代碼邏輯,提高線程安全性和程序的可靠性。 (智能指針原理也是類似的)
testMutex.cpp
#include <iostream>
#include <mutex>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include "Mutex.hpp"using namespace MutexModule;int ticket = 1000;class ThreadData
{
public:ThreadData(const std::string &n, Mutex &lock): name(n),lockp(&lock){}~ThreadData() {}std::string name;Mutex *lockp;
};// 加鎖:盡量加鎖的范圍粒度要比較細,盡可能的不要包含太多的非臨界區代碼
void *route(void *arg)
{ThreadData *td = static_cast<ThreadData *>(arg);while (1){LockGuard guard(*td->lockp); // 加鎖完成, RAII風格的互斥鎖的實現if (ticket > 0){usleep(1000);printf("%s sells ticket:%d\n", td->name.c_str(), ticket);ticket--;}else{break;}usleep(123);}return nullptr;
}int main(void)
{Mutex lock;pthread_t t1, t2, t3, t4;ThreadData *td1 = new ThreadData("thread 1", lock);pthread_create(&t1, NULL, route, td1);ThreadData *td2 = new ThreadData("thread 2", lock);pthread_create(&t2, NULL, route, td2);ThreadData *td3 = new ThreadData("thread 3", lock);pthread_create(&t3, NULL, route, td3);ThreadData *td4 = new ThreadData("thread 4", lock);pthread_create(&t4, NULL, route, td4);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);return 0;
}
?