[操作系統] 線程互斥


文章目錄

  • 背景概念
  • 線程互斥的引出
  • 互斥量
    • 鎖的操作
      • 初始化 (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) 這個判斷。

  1. Thread A 執行 if (ticket > 0)
    • Thread A 讀取內存中的 ticket 值,發現是 1。
    • 1 大于 0,條件為真。Thread A 準備進入 if 塊內的代碼。
  2. Thread B 執行 if (ticket > 0)
    • 就在 Thread A 進入 if 塊之后(可能被usleep卡住),操作系統切換到 Thread B。
    • Thread B 也讀取內存中的 ticket 值,此時內存中的 ticket 仍然是 1(因為 Thread A 還沒修改它)。
    • 1 大于 0,條件為真。Thread B 也****進入 if 塊內的代碼。

現在,兩個線程都通過了 if (ticket > 0) 的檢查,都認為自己可以售票。它們將相繼準備執行搶票。

假設接下來 Thread A 先執行 ticket-- 的過程:

  1. Thread A 執行 ticket-- :
    • Thread A 執行ticket--,現在ticket變為0
  2. 操作系統切換到 Thread B。
  3. Thread B 執行 ticket--:
    • Thread B 執行ticket--,現在ticket變為-1

這個簡化的例子說明了,由于線程切換可能發生在代碼的任何地方,多個線程可能讀取到同一個舊的 ticket 值,全部進入臨界區,各自進行減一操作,然后將減一后的值寫回內存。最終的結果是 ticket 被“超賣”了,賣出的總票數超過了初始值 100,從而出現了負數。

從代碼表面理解如此,實際上在匯編實現上也會進行線程的切換,不同的是每條匯編指令是原子的

這就是為什么對共享變量的非原子操作在多線程環境下需要同步控制,以確保同一時間只有一個線程能夠完成整個操作序列,避免這種錯誤的交錯執行。


解決臨界區問題,本質上就是需要一個鎖將該區域鎖起來,Linux提供的鎖叫互斥量

互斥量

  • 互斥量 (Mutex): 一把用于多線程同步的鎖,確保在任何時刻只有一個線程可以訪問被保護的共享資源。
  • 臨界區 (Critical Section): 訪問共享資源的代碼段,需要用互斥量來保護。

鎖的操作

互斥量的主要操作:

  1. 初始化 (Initialization)
  2. 加鎖 (Locking)
  3. 解鎖 (Unlocking)
  4. 銷毀 (Destruction)
  5. 設置屬性 (Setting Attributes - 影響互斥量行為)

初始化 (Initialization)

在使用互斥量之前,必須先進行初始化。有兩種主要的初始化方式:

靜態初始化
  • 操作: 使用宏 PTHREAD_MUTEX_INITIALIZERpthread_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);
}

細節補充

如果對一個臨界區加鎖之后,在臨界區內部執行時允許線程切換嗎?切換后會怎么樣?

允許切換!并且不會對造成影響。

因為當前進程尚未釋放鎖,鎖仍然被當前進程持有,具體持有邏輯會在下文互斥量的實現原理中理解。反正,只要該線程持有鎖,其他線程只能等待線程執行完臨界區后解鎖,才能申請加鎖。

即使可以切換,每一次切換后要申請鎖的時候會檢測到該鎖已經被申請走,然后就會將該線程阻塞掛起。

這是每個線程都要遵守的規則!!


互斥量的實現原理

實現鎖有兩種方法:

  1. 硬件實現:只要關閉時鐘中斷就可以避免線程切換,從而保護臨界區;
  2. 軟件實現:該章節講解重點。

執行流的上下文

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. 準備寄存器: 線程 1 的 CPU 執行 movb $0, %al,將線程 1 的 al 寄存器的值設置為 0。
  2. 原子交換 (xchgb %al, mutex): 這是關鍵的原子操作。CPU 執行 xchgb 指令,它會在一個不可分割的步驟中完成兩件事:
    • 將內存中 mutex 的當前值 (1) 讀取到線程 1 的 al 寄存器中。
    • 將線程 1 的 al 寄存器中原來的值 (0) 寫入到內存的 mutex 地址。
    • 重要: 這個讀取和寫入是作為一個整體完成的,期間不會被其他線程的 xchgb 操作打斷。
    • 交換后: 線程 1 的 al 寄存器現在的值是 1,內存中 mutex 的值現在是 0。
  3. 檢查舊值: 線程 1 接著執行 if(al寄存器的內容 > 0)。由于此時線程 1 的 al 寄存器值是 1 (這是交換前 mutex 的值),條件 1 > 0 為真。
  4. 加鎖成功并返回: 條件為真,線程 1 認為自己成功獲取了鎖,執行 return 0,進入臨界區。

線程切換發生,線程 1 的上下文保存:

  • 操作系統決定進行線程切換。線程 1 當前在 CPU 上的狀態,包括所有寄存器(如 al,此時值為 1)和程序計數器等,都會被保存到線程 1 自己的上下文結構中。您說的“線程1會將自己所維護的上下文,也就是當前CPU寄存器的數據全部帶走”是正確的,這些數據是線程私有的,在切換時會被保存。

線程 2 嘗試加鎖:

  • 操作系統將線程 2 的上下文加載到 CPU 的寄存器中。此時 CPU 的 al 寄存器和程序計數器等都變成了線程 2 的狀態。
  1. 準備寄存器: 線程 2 的 CPU 執行 movb $0, %al,將線程 2 的 al 寄存器的值設置為 0。
  2. 原子交換 (xchgb %al, mutex): 線程 2 執行 xchgb 指令。
    • 注意: 此時內存中 mutex 的值是 0 (因為之前線程 1 已經將其設為 0)。
    • CPU 執行原子交換:將內存中 mutex 的當前值 (0) 讀取到線程 2 的 al 寄存器中,同時將線程 2 的 al 寄存器中原來的值 (0) 寫入到內存的 mutex 地址。
    • 交換后: 線程 2 的 al 寄存器現在的值是 0,內存中 mutex 的值仍然是 0。
  3. 檢查舊值: 線程 2 執行 if(al寄存器的內容 > 0)。由于此時線程 2 的 al 寄存器值是 0 (這是交換前 mutex 的值),條件 0 > 0 為假。
  4. 加鎖失敗并等待: 條件為假,線程 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。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/79275.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/79275.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/79275.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【神經網絡與深度學習】兩種加載 pickle 文件方式(joblib、pickle)的差異

引言 從深度學習應用到數據分析的多元化需求出發&#xff0c;Python 提供了豐富的工具和模塊&#xff0c;其中 pickle 和 joblib 兩種方式在加載數據文件方面表現尤為突出。不同場景對性能、兼容性以及后續處理的要求不盡相同&#xff0c;使得這兩種方式各顯優勢。本文將通過深…

Electron 入門指南

Electron 入門指南 Electron 是一個使用 JavaScript、HTML 和 CSS 構建跨平臺桌面應用的框架。通過 Electron&#xff0c;你可以利用 Web 技術開發出功能強大的桌面應用程序&#xff0c;并且能夠運行在 Windows、Mac 和 Linux 系統上。 本文將帶你從零開始構建一個簡單的 Ele…

編程中如何與AI交互-結構化輸入和理解確認機制

一 結構化輸入是什么 &#x1f4cc; 結構化輸入的定義&#xff1a; 結構化輸入是指以清晰、分層、有邏輯的格式向 AI 輸入信息&#xff0c;使其更容易解析內容、抓住重點&#xff0c;并準確回答問題。 &#x1f4e6; 舉個例子&#xff08;編程場景&#xff09;&#xff1a; 非…

13:傅里葉變換

傅立葉變換(FT, Fourier Transform)的作用是將一個信號由時域變換到頻域。其實就是把數據由橫坐標時間、縱坐標采樣值的波形圖格式&#xff0c;轉換為橫坐標頻率、縱坐標振幅(或相位)的頻譜格式。換后可以很明顯地看出一些原先不易察覺的特征。 有些信號在時域上是很難看出什么…

基于單片機的音頻信號處理系統設計(一)

項目名稱:基于單片機的音頻信號處理系統設計學院名稱:信息學院學生姓名:學號專業年級:指導教師:教師職稱:教授企業導師:目 錄 摘 要 Abstract 1 前言 1.1研究背景與意義 <

機器學習實操 第一部分 機器學習基礎 第8章 降維技術

機器學習實操 第一部分 機器學習基礎 第8章 降維技術 內容概要 第8章探討了降維技術&#xff0c;這些技術在處理高維數據時至關重要。高維數據不僅會使訓練過程變得極其緩慢&#xff0c;還會增加找到良好解決方案的難度&#xff0c;這就是所謂的維度災難問題。幸運的是&#…

微信小程序 XSS 防護知識整理

場景1&#xff1a;用戶輸入表單&#xff08;如評論框&#xff09; 錯誤做法&#xff1a;直接渲染未過濾的用戶輸入 // WXML <view>{{ userInput }}</view>// JS&#xff08;用戶輸入了惡意內容&#xff09; Page({data: { userInput: <script>alert("…

MySQL 服務搭建

&#x1f4a2;歡迎來到張翊塵的開源技術站 &#x1f4a5;開源如江河&#xff0c;匯聚眾志成。代碼似星辰&#xff0c;照亮行征程。開源精神長&#xff0c;傳承永不忘。攜手共前行&#xff0c;未來更輝煌&#x1f4a5; 文章目錄 在線安裝Ubuntu/Debian更新系統包索引安裝 MySQL …

【Java面試筆記:進階】23.請介紹類加載過程,什么是雙親委派模型?

Java的類加載機制是JVM的核心組成部分,其過程分為三個階段,并采用雙親委派模型來保證類加載的安全性和一致性。 1.類加載過程 1.加載階段(Loading) 核心任務:查找并加載類的二進制字節流(如.class文件)。具體行為: 將字節碼數據從不同數據源(如文件系統、網絡等)讀…

UN R79 關于車輛轉向裝置形式認證的統一規定(正文部分1)

UN R79法規是針對轉向裝置的型式認證法規&#xff0c;涉及A/B1/C類的橫向控制輔助駕駛功能&#xff0c;對各功能的功能邊界、性能要求、狀態提示、故障警示以及型式認證要提交的信息做了規范&#xff0c;本文結合百度文心一言對法規進行翻譯&#xff0c;并結合個人理解對部分內…

[隨筆] 升級uniapp舊項目的vue、pinia、vite、dcloudio依賴包等

匯總 # 升級uniapp項目dcloudio整體依賴&#xff0c;建議執行多次 # 會順帶自動更新/升級vue的版本 npx dcloudio/uvmlatest alpha# 檢查 pinia 的最新版本 npm view pinia version# 更新項目 pinia 到最新版本 npm update pinia# 更新項目 pinia 到特定的版本 # 首先&#xf…

【使用小皮面板 + WordPress 搭建本地網站教程】

&#x1f680; 使用小皮面板 WordPress 搭建本地網站教程&#xff08;快速上手&#xff09; 本教程將手把手教你如何使用 小皮面板&#xff08;XAMPP 類似工具&#xff09; 和 WordPress 搭建一個完全本地化的網站環境。適合 初學者 / 博主 / Web開發者 本地練習使用&#xf…

[更新完畢]2025五一杯A題五一杯數學建模思路代碼文章教學:支路車流量推測問題

完整內容請看文章最下面的推廣群 支路車流量推測問題 摘要 本文針對支路車流量推測問題展開研究&#xff0c;通過建立數學模型解決不同場景下的車流量分析需求。 針對問題一&#xff08;Y型道路場景&#xff09;&#xff0c;研究兩支路匯入主路的車流量推測。通過建立線性增長…

前端面試寶典---webpack原理解析,并有簡化版源碼

前言 先看一下webpack打包后的bundle.js&#xff0c;前邊的直接掃一眼就過&#xff0c;可以發現這個立即執行函數的形參就是一個&#xff0c;key為引入文件路徑&#xff0c;value為該模塊代碼的函數。 所以比較重要的就是通過webpack的配置文件中的entry的入口文件&#xff0c…

面試的各種類型

面試是用人單位選拔人才的重要環節&#xff0c;常見的面試類型有結構化面試、半結構化面試、非結構化面試和壓力面試&#xff0c;每種類型都有其特點和應對策略。 一、結構化面試 特點&#xff1a; 標準化流程 面試流程固定&#xff0c;考官會按照預先設計好的問題清單依次向…

vue3定義全局防抖指令

文章目錄 代碼參數講解 在寫項目時&#xff0c;總會有要進行防抖節流的時候&#xff0c;如果寫一個debounce函數的話 用起來代碼總會是有點長的&#xff0c;因此想到了用一個全局指令進行輸入框的防抖&#xff0c;畢竟全局指令使用時只要v-xxx就行了&#xff0c;非常方便 代碼…

WebDeveloper 流量分析、sudo提權,靶場通關WP

一、信息收集 1、主機探測 arp-scan -l netdiscover -i eth0 -r 192.168.33.0/24 nmap -sP 192.168.66.0/24 2、端口掃描 nmap -sS -sV 192.168.66.141 PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4 (Ubuntu Linux; protocol 2.0) 80/tcp op…

某化工廠運維升級:智和信通運維平臺實現工業交換機智能管理

隨著某化工廠數字化轉型的加速推進&#xff0c;其生產過程對復雜網絡和IT設備的依賴程度日益加深。當前的網絡不僅承載著生產控制系統&#xff08;如DCS、PLC等&#xff09;的通信需求&#xff0c;還同時支持辦公自動化、安防監控、工業物聯網&#xff08;IoT&#xff09;等多種…

React:封裝一個編輯文章的組件

封裝一個編輯文章的組件,就要用到富文本編輯器,支持標題、內容、標簽等的編輯,并且能夠保存和取消。 首先,我需要考慮用戶的具體需求。編輯文章組件通常需要哪些功能?標題輸入、內容編輯、標簽管理、保存和取消按鈕。可能還需要自動保存草稿、驗證輸入、錯誤提示等功能。用…

數據結構與算法:圖論——并查集

先給出并查集的模板&#xff0c;還有一些leetcode算法題&#xff0c;以后遇見了相關題目再往上增加 并查集模板 整體模板C代碼如下&#xff1a; 空間復雜度&#xff1a; O(n) &#xff0c;申請一個father數組。 時間復雜度 路徑壓縮后的并查集時間復雜度在O(logn)與O(1)之間…