文章目錄
- 背景概念
- 線程互斥的引出
- 互斥量
- 鎖的操作
- 初始化 (Initialization)
- 靜態初始化
- 動態初始化
- 加鎖 (Locking)
- 阻塞式加鎖
- 非阻塞式加鎖 (嘗試加鎖/一般不考慮)
- 解鎖 (Unlocking)
- 銷毀 (Destruction)
- 設置屬性 (Setting Attributes - 通過 `pthread_mutex_init`)
- 鎖本身的保護
- 互斥鎖的應用
- 細節補充
- 互斥量的實現原理
- 執行流的上下文
- 互斥鎖的實現
- 互斥量的封裝
背景概念
- 臨界資源:多線程執行流共享的資源就叫做臨界資源
- 臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區
- 互斥:任何時刻,互斥保證有且只有?個執?流進?臨界區,訪問臨界資源,通常對臨界資源起保護作?
- 原?性(后?討論如何實現):不會被任何調度機制打斷的操作,該操作只有兩態,要么完成,要么未完成,不會在執行期間進行中斷。
線程互斥的引出
以下代碼模擬一個售票系統,有一個全局變量tickled
,所有的線程進行搶票,每次搶后進行--
。
// 操作共享變量會有問題的售票系統代碼
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>int ticket = 1000;void *route(void *arg)
{char *id = (char *)arg;while (1){if (ticket > 0) // 1. 判斷{usleep(1000); // 模擬搶票花的時間printf("%s sells ticket:%d\n", id, ticket); // 2. 搶到了票ticket--; // 3. 票數--}else{break;}}return nullptr;
}int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
正常情況下每個線程會去搶奪票,然后--
,但是發現當票數為0的時候并沒有停止,而是結果如下:
出現了負數。
我們將聚焦點放在每個線程的搶票的代碼邏輯部分:
if (ticket > 0) // 1. 判斷
{usleep(1000); // 模擬搶票花的時間printf("%s sells ticket:%d\n", id, ticket); // 2. 搶到了票ticket--; // 3. 票數--
}
else
{break;
}
首先可以明確,該代碼中的臨界資源是ticket
,因為ticket
是線程之間所需要共享進行--
的變量。而線程內部進行訪問臨界資源的代碼是:
if (ticket > 0) // 1. 判斷
{usleep(1000); // 模擬搶票花的時間printf("%s sells ticket:%d\n", id, ticket); // 2. 搶到了票ticket--; // 3. 票數--
}
所以說會出現負數結果,問題一定是在臨界區,說明臨界區沒有進行保護,也就是互斥!
那么這段臨界區為什么沒有保護好臨界資源,到底發生了什么?
線程切換的時間點:1??時間片到期 2??阻塞式IO 3??sleep等系統調用陷入內核
好的,我們來用一個更簡化的方式,只關注寄存器的存取過程,模擬 ticket
從一個小正數變成負數的情況。
假設 ticket
變量當前的內存值為 **1,**還剩一張票。
現在,有兩個線程,Thread A 和 Thread B,幾乎同時執行到 if (ticket > 0)
這個判斷。
- Thread A 執行
if (ticket > 0)
:- Thread A 讀取內存中的
ticket
值,發現是 1。 - 1 大于 0,條件為真。Thread A 準備進入
if
塊內的代碼。
- Thread A 讀取內存中的
- Thread B 執行
if (ticket > 0)
:- 就在 Thread A 進入
if
塊之后(可能被usleep
卡住),操作系統切換到 Thread B。 - Thread B 也讀取內存中的
ticket
值,此時內存中的ticket
仍然是 1(因為 Thread A 還沒修改它)。 - 1 大于 0,條件為真。Thread B 也****進入
if
塊內的代碼。
- 就在 Thread A 進入
現在,兩個線程都通過了 if (ticket > 0)
的檢查,都認為自己可以售票。它們將相繼準備執行搶票。
假設接下來 Thread A 先執行 ticket--
的過程:
- Thread A 執行
ticket--
:- Thread A 執行
ticket--
,現在ticket
變為0
。
- Thread A 執行
- 操作系統切換到 Thread B。
- Thread B 執行
ticket--
:- Thread B 執行
ticket--
,現在ticket
變為-1
。
- Thread B 執行
這個簡化的例子說明了,由于線程切換可能發生在代碼的任何地方,多個線程可能讀取到同一個舊的 ticket
值,全部進入臨界區,各自進行減一操作,然后將減一后的值寫回內存。最終的結果是 ticket
被“超賣”了,賣出的總票數超過了初始值 100,從而出現了負數。
從代碼表面理解如此,實際上在匯編實現上也會進行線程的切換,不同的是每條匯編指令是原子的。
這就是為什么對共享變量的非原子操作在多線程環境下需要同步控制,以確保同一時間只有一個線程能夠完成整個操作序列,避免這種錯誤的交錯執行。
解決臨界區問題,本質上就是需要一個鎖將該區域鎖起來,Linux提供的鎖叫互斥量。
互斥量
- 互斥量 (Mutex): 一把用于多線程同步的鎖,確保在任何時刻只有一個線程可以訪問被保護的共享資源。
- 臨界區 (Critical Section): 訪問共享資源的代碼段,需要用互斥量來保護。
鎖的操作
互斥量的主要操作:
- 初始化 (Initialization)
- 加鎖 (Locking)
- 解鎖 (Unlocking)
- 銷毀 (Destruction)
- 設置屬性 (Setting Attributes - 影響互斥量行為)
初始化 (Initialization)
在使用互斥量之前,必須先進行初始化。有兩種主要的初始化方式:
靜態初始化
- 操作: 使用宏
PTHREAD_MUTEX_INITIALIZER
對pthread_mutex_t
變量進行賦值。 - 代碼示例:
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
- 講解:
- 這種方式主要用于全局或靜態存儲期的互斥量。
- 它在程序啟動時自動完成初始化。
- 使用這種方式初始化的互斥量,通常不需要顯式調用
pthread_mutex_destroy()
進行銷毀,系統會在程序結束時自動清理。 - 這種方式初始化的互斥量通常是默認類型(Normal 或 Fast 互斥量),不支持特殊屬性(如遞歸)。
動態初始化
- 操作: 調用
pthread_mutex_init()
函數。 - 函數原型:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
- 代碼示例:
pthread_mutex_t my_mutex;
// ... 可能設置屬性 ...
int ret = pthread_mutex_init(&my_mutex, NULL); // 使用默認屬性初始化
if (ret != 0) {// 處理錯誤
}
- 講解:
- 這種方式用于局部變量或通過
malloc
等動態分配的互斥量。 - 需要在代碼中顯式調用此函數進行初始化。
mutex
: 指向要初始化的pthread_mutex_t
變量的指針。attr
: 指向互斥量屬性對象的指針。如果為NULL
,則使用默認屬性。可以通過創建并設置pthread_mutexattr_t
對象來指定互斥量類型(如遞歸、錯誤檢查)或進程共享屬性。- 使用動態初始化方式的互斥量,在不再需要時必須顯式調用
pthread_mutex_destroy()
進行銷毀,以釋放其占用的系統資源。
- 這種方式用于局部變量或通過
加鎖 (Locking)
加鎖是為了獲取對共享資源的獨占訪問權,進入臨界區。有兩種主要的加鎖操作:
阻塞式加鎖
- 操作: 調用
pthread_mutex_lock()
函數。 - 函數原型:
int pthread_mutex_lock(pthread_mutex_t *mutex);
- 代碼示例:
// 嘗試獲取鎖
int ret = pthread_mutex_lock(&my_mutex);
if (ret == 0) {// 成功獲取鎖,進入臨界區// ... 訪問共享資源 ...// 釋放鎖pthread_mutex_unlock(&my_mutex);
} else {// 處理錯誤
}
- 講解:
- 這是最常用的加鎖方式。
mutex
: 指向要加鎖的互斥量的指針。- 如果互斥量當前沒有被任何線程持有,調用線程將立即成功獲取鎖,函數返回 0。
- 如果互斥量已經被其他線程持有,調用線程將被阻塞(暫停執行),直到持有鎖的線程調用
pthread_mutex_unlock()
釋放鎖。一旦鎖被釋放,被阻塞的線程之一(具體哪個取決于調度策略)將被喚醒并獲得鎖。 - 原子性: 加鎖操作本身是原子的,即檢查鎖狀態并獲取鎖的過程是不可分割的。
非阻塞式加鎖 (嘗試加鎖/一般不考慮)
- 操作: 調用
pthread_mutex_trylock()
函數。 - 函數原型:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
- 代碼示例:
// 嘗試獲取鎖,如果獲取不到立即返回
int ret = pthread_mutex_trylock(&my_mutex);
if (ret == 0) {// 成功獲取鎖,進入臨界區// ... 訪問共享資源 ...// 釋放鎖pthread_mutex_unlock(&my_mutex);
} else if (ret == EBUSY) {// 鎖當前被其他線程持有,未獲取到鎖// ... 執行其他非臨界區任務或稍后重試 ...
} else {// 處理其他錯誤
}
- 講解:
mutex
: 指向要嘗試加鎖的互斥量的指針。- 如果互斥量當前沒有被任何線程持有,調用線程將成功獲取鎖,函數返回 0。
- 如果互斥量已經被其他線程持有,調用線程將立即返回錯誤碼
EBUSY
,而不會阻塞。 - 這種方式適用于希望在無法立即獲取鎖時避免線程阻塞,從而保持程序的響應性或執行其他并行任務的場景。
解鎖 (Unlocking)
解鎖是釋放對共享資源的獨占訪問權,離開臨界區。
- 操作: 調用
pthread_mutex_unlock()
函數。 - 函數原型:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
- 代碼示例:
// ... 訪問共享資源 ...
// 釋放鎖
int ret = pthread_mutex_unlock(&my_mutex);
if (ret != 0) {// 處理錯誤(例如,嘗試解鎖非自己持有的鎖)
}
- 講解:
mutex
: 指向要解鎖的互斥量的指針。- 通常,**只有持有互斥量的線程才能成功調用 **
pthread_mutex_unlock()
。嘗試解鎖一個未加鎖或由其他線程持有的互斥量是錯誤的行為(對于默認類型的互斥量,這會導致未定義行為;對于錯誤檢查類型的互斥量,會返回錯誤)。 - 解鎖后,如果之前有線程因為嘗試獲取該互斥量而被阻塞,其中一個線程將被喚醒并獲得鎖。
- 原子性: 解鎖操作本身也是原子的。
銷毀 (Destruction)
銷毀互斥量是釋放其占用的系統資源。
- 操作: 調用
pthread_mutex_destroy()
函數。 - 函數原型:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
- 代碼示例:
// ... 使用完互斥量后 ...
int ret = pthread_mutex_destroy(&my_mutex);
if (ret != 0) {// 處理錯誤
}
- 講解:
mutex
: 指向要銷毀的互斥量的指針。- 只應該銷毀通過
pthread_mutex_init()
動態初始化的互斥量。 對于通過PTHREAD_MUTEX_INITIALIZER
靜態初始化的互斥量,通常不需要手動銷毀。 - 銷毀一個已經被加鎖的互斥量或有線程正在等待的互斥量會導致未定義行為,所以在銷毀前必須確保互斥量處于未加鎖狀態且沒有線程在等待它。
設置屬性 (Setting Attributes - 通過 pthread_mutex_init
)
雖然不是直接的鎖操作,但互斥量的行為可以通過初始化時的屬性來控制。這適用于動態初始化。
- 相關函數:
pthread_mutexattr_init()
,pthread_mutexattr_settype()
,pthread_mutexattr_destroy()
, 等。 - 講解:
- 可以通過創建并設置
pthread_mutexattr_t
對象來指定互斥量的類型。常見的類型有:PTHREAD_MUTEX_NORMAL
** (或默認)😗* 標準互斥量。如果同一個線程多次加鎖會導致死鎖。嘗試解鎖非自己持有的鎖是未定義行為。PTHREAD_MUTEX_RECURSIVE
** (遞歸)😗* 允許同一個線程多次加鎖,需要進行相同次數的解鎖。適用于函數內部調用需要加鎖的另一個函數的情況。PTHREAD_MUTEX_ERRORCHECK
** (錯誤檢查)😗* 對使用錯誤(如重復加鎖、解鎖非自己持有的鎖)進行檢查并返回錯誤碼,有助于調試。
- 還可以設置進程共享屬性,使得互斥量可以在不同進程之間使用。
- 可以通過創建并設置
鎖本身的保護
當申請競爭鎖的時候,申請鎖的過程本身就是臨界的,所以該過程也需要被保護起來,該過程必須為原子的。
怎么理解“申請鎖的過程本身就是臨界的”?
當我們調用 pthread_mutex_lock()
函數時,底層會發生一系列操作來嘗試獲取這把鎖。這個過程大致可以概括為:
- 檢查鎖的狀態: 查看互斥量當前是空閑(unlocked)還是已被占用(locked)。
- 如果鎖是空閑的: 將鎖的狀態設置為已占用,表示當前線程成功獲得了鎖。
- 如果鎖已被占用: 線程進入等待狀態(通常會被放入一個等待隊列),直到鎖被釋放。
想象一下,如果有兩個線程(Thread A 和 Thread B)同時調用 pthread_mutex_lock(&mutex)
,并且此時 mutex
正好是空閑的。
- Thread A 讀取
mutex
的狀態,發現是空閑。 - 操作系統發生線程切換。
- Thread B 讀取
mutex
的狀態,發現仍然是空閑(因為 Thread A 還沒來得及將狀態設為已占用)。 - Thread B 將
mutex
的狀態設置為已占用,認為自己獲取了鎖。 - 操作系統切換回 Thread A。
- Thread A 將
mutex
的狀態設置為已占用,也認為自己獲取了鎖。
這樣,兩個線程都錯誤地認為自己成功獲取了鎖,都可以進入它們試圖保護的應用程序層面的臨界區,這就會導致數據競爭和不一致問題。
所以,“申請鎖的過程”——即檢查鎖狀態并根據狀態決定是否獲取鎖并修改鎖狀態的這個過程——本身就是一個對互斥量這個“共享資源”(互斥量的數據結構本身)的訪問和修改過程。多個線程同時進行這個過程,同樣面臨競態條件。因此,申請鎖的過程本身就是一個需要被保護的臨界區。
怎么解決“該過程必須為原子的”?
要解決申請鎖過程的競態問題,就需要保證檢查鎖狀態并設置鎖狀態(或者更一般地,測試并設置)這個操作是原子的。這意味著,當一個線程正在執行這個“測試并設置”操作時,其他線程不能打斷它,也不能同時執行這個操作。這個操作要么完全成功,要么完全失敗,不會出現執行到一半被切換的情況。
這個問題不是通過在應用程序層面再加一個鎖來解決的(那樣會導致無限套娃),而是通過依賴底層硬件提供的原子操作指令來解決。現代CPU提供了一些特殊的指令,這些指令能夠在一個不可中斷的步驟中完成對內存位置的讀取、修改和寫回操作。
常見的硬件原子操作指令包括:
- Test-and-Set (測試并設置): 讀取內存位置的值,并將其設置為某個新值(通常是 1),整個過程是原子的。該指令通常會返回內存位置的舊值,調用者可以根據返回的舊值來判斷是否成功獲取了鎖。
- Compare-and-Swap (CAS, 比較并交換): 原子地比較內存位置的當前值與一個期望值,如果相等,則將該內存位置的值更新為一個新值。該指令通常也會返回內存位置的當前值,調用者可以通過比較返回的值與期望值來判斷操作是否成功。
下文會對互斥量原理進行詳細講解。
互斥鎖的應用
將上述代碼的問題用鎖來解決,代碼如下:
// 操作共享變量會有問題的售票系統代碼
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <iostream>int ticket = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;void *route(void *arg)
{char *id = (char *)arg;while (1){ pthread_mutex_lock(&lock);if (ticket > 0) // 1. 判斷{usleep(1000); // 模擬搶票花的時間printf("%s sells ticket:%d\n", id, ticket); // 2. 搶到了票ticket--; // 3. 票數--pthread_mutex_unlock(&lock);}else{pthread_mutex_unlock(&lock);break; }}return nullptr;
}int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, (void *)"thread 1");pthread_create(&t2, NULL, route, (void *)"thread 2");pthread_create(&t3, NULL, route, (void *)"thread 3");pthread_create(&t4, NULL, route, (void *)"thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
細節補充
如果對一個臨界區加鎖之后,在臨界區內部執行時允許線程切換嗎?切換后會怎么樣?
允許切換!并且不會對造成影響。
因為當前進程尚未釋放鎖,鎖仍然被當前進程持有,具體持有邏輯會在下文互斥量的實現原理中理解。反正,只要該線程持有鎖,其他線程只能等待線程執行完臨界區后解鎖,才能申請加鎖。
即使可以切換,每一次切換后要申請鎖的時候會檢測到該鎖已經被申請走,然后就會將該線程阻塞掛起。
這是每個線程都要遵守的規則!!
互斥量的實現原理
實現鎖有兩種方法:
- 硬件實現:只要關閉時鐘中斷就可以避免線程切換,從而保護臨界區;
- 軟件實現:該章節講解重點。
執行流的上下文
CPU內只有一套寄存器,但是可以有多套數據。
每個進程/線程都有一套自己的上下文數據,每次在線程切換的時候就會將當前寄存器的數據打包帶走,調度到下一個線程的時候,這個線程會將自己的上下文數據更新到CPU寄存器內。
上下文數據屬于線程私有的,寄存器只是臨時存儲!
我們使用swap,exchange將內存中的互斥鎖數據(變量)交換到CPU的寄存器中,本質就是當前線程在申請鎖,因為鎖的數據是唯一的,誰占有這個鎖的數據,誰就擁有鎖,當切換線程后發現鎖的數據已經不在了,就是已經被占用,也就會執行掛起阻塞!
下文詳細講解該過程。
互斥鎖的實現
互斥鎖操作的實現,大多數體系結構都提供了swap
或者exchange
指令,該指令的作用是把寄存器和內存單元的數據相交換。
lock
偽代碼:
lock:movb $0, %al ; 1. 準備寄存器:將 al 寄存器清零xchgb %al, mutex ; 2. 原子交換:將 al 寄存器的值與內存中的 mutex 值進行交換if(al寄存器的內容 > 0){ ; 3. 檢查舊值:判斷交換前 mutex 的值(現在在 al 中)是否大于 0return 0; ; 如果大于 0 (鎖之前是空閑的),加鎖成功} else {掛起等待; ; 如果小于等于 0 (鎖之前是占用的),掛起等待goto lock; ; 繼續嘗試獲取鎖 (這里簡化為自旋或等待后重試)}
unlock
偽代碼:
unlock:movb $1, mutex ; 釋放鎖:將內存中的 mutex 值設為 1 (表示空閑)喚醒等待Mutex的線程; ; 喚醒:通知等待在該互斥量上的線程return 0;
假設 mutex
變量在內存中,初始值為 1 (代表鎖是空閑的)。
線程 1 嘗試加鎖:
- 準備寄存器: 線程 1 的 CPU 執行
movb $0, %al
,將線程 1 的al
寄存器的值設置為 0。 - 原子交換 (
xchgb %al, mutex
): 這是關鍵的原子操作。CPU 執行xchgb
指令,它會在一個不可分割的步驟中完成兩件事:- 將內存中
mutex
的當前值 (1) 讀取到線程 1 的al
寄存器中。 - 將線程 1 的
al
寄存器中原來的值 (0) 寫入到內存的mutex
地址。 - 重要: 這個讀取和寫入是作為一個整體完成的,期間不會被其他線程的
xchgb
操作打斷。 - 交換后: 線程 1 的
al
寄存器現在的值是 1,內存中mutex
的值現在是 0。
- 將內存中
- 檢查舊值: 線程 1 接著執行
if(al寄存器的內容 > 0)
。由于此時線程 1 的al
寄存器值是 1 (這是交換前mutex
的值),條件1 > 0
為真。 - 加鎖成功并返回: 條件為真,線程 1 認為自己成功獲取了鎖,執行
return 0
,進入臨界區。
線程切換發生,線程 1 的上下文保存:
- 操作系統決定進行線程切換。線程 1 當前在 CPU 上的狀態,包括所有寄存器(如
al
,此時值為 1)和程序計數器等,都會被保存到線程 1 自己的上下文結構中。您說的“線程1會將自己所維護的上下文,也就是當前CPU寄存器的數據全部帶走”是正確的,這些數據是線程私有的,在切換時會被保存。
線程 2 嘗試加鎖:
- 操作系統將線程 2 的上下文加載到 CPU 的寄存器中。此時 CPU 的
al
寄存器和程序計數器等都變成了線程 2 的狀態。
- 準備寄存器: 線程 2 的 CPU 執行
movb $0, %al
,將線程 2 的al
寄存器的值設置為 0。 - 原子交換 (
xchgb %al, mutex
): 線程 2 執行xchgb
指令。- 注意: 此時內存中
mutex
的值是 0 (因為之前線程 1 已經將其設為 0)。 - CPU 執行原子交換:將內存中
mutex
的當前值 (0) 讀取到線程 2 的al
寄存器中,同時將線程 2 的al
寄存器中原來的值 (0) 寫入到內存的mutex
地址。 - 交換后: 線程 2 的
al
寄存器現在的值是 0,內存中mutex
的值仍然是 0。
- 注意: 此時內存中
- 檢查舊值: 線程 2 執行
if(al寄存器的內容 > 0)
。由于此時線程 2 的al
寄存器值是 0 (這是交換前mutex
的值),條件0 > 0
為假。 - 加鎖失敗并等待: 條件為假,線程 2 知道鎖已經被占用了,執行
else
塊中的“掛起等待”和goto lock
。在實際的互斥量實現中,這通常意味著線程 2 會被放入一個等待隊列,并讓出 CPU,進入睡眠狀態,而不是像goto lock
這樣忙等(自旋)。
解鎖:
即使加鎖后線程執行的代碼可能會影響al寄存器,但是最后的解鎖操作,是直接將1寫入mutex
,而不是交換,所以無論怎樣都不會影響解鎖操作。
總結:
- 鎖的狀態判斷和修改是原子的:
xchgb
指令保證了“讀取舊值”和“寫入新值”這兩個步驟是捆綁在一起不可分割的。 - 誰先到達誰成功: 多個線程同時執行
xchgb
時,只有一個線程能成功地將內存中原來的鎖狀態從“空閑”(1) 換到自己的寄存器中。 - 通過檢查寄存器的舊值判斷是否獲取鎖: 哪個線程在
xchgb
后發現自己的al
寄存器里是舊的“空閑”狀態 (1),就說明它成功地將鎖的狀態改為了“占用” (0),從而獲取了鎖。 - 其他線程等待: 其他線程在
xchgb
后發現自己的al
寄存器里是舊的“占用”狀態 (0),就知道鎖已經被別人拿走了,只能等待。 - 上下文保存和恢復: 線程切換時,寄存器狀態等上下文信息確實會被保存和恢復,這是操作系統實現多任務的基礎。這并不會影響鎖機制的正確性,因為鎖的狀態是保存在所有線程共享的內存中的,而
xchgb
操作保證了對內存鎖狀態的修改是原子的。
加鎖和解鎖的形象圖示如下:
互斥量的封裝
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>namespace MutexModule
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}~Mutex(){pthread_mutex_destroy(&_mutex);}private:pthread_mutex_t _mutex;};class LockGuard{public:LockGuard(Mutex &mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}private:Mutex &_mutex;};
}
testMutex.cc
#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;
// pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;
// std::mutex cpp_lock;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)
{// pthread_mutex_t lock;// pthread_mutex_init(&lock, nullptr); // 初始化鎖{int a = 10;}int a = 20;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);// pthread_mutex_destroy(&lock);return 0;
}
封裝中有一個C++的RAII細節可以注意學習:
class LockGuard
{
public:LockGuard(Mutex &mutex):_mutex(mutex){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}
private:Mutex &_mutex;
};
通過LockGuard
的封裝,在使用Mutex
的時候,使用局部的LockGuard
:
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);
}
當一次循環結束后,guard
的析構函數就會自動調用Mutex
的析構函數,進而釋放鎖,到達自動關鎖的目的,類似于智能指針,這就是RAII。