線程同步與互斥(上)

上一篇:線程概念與控制https://blog.csdn.net/Small_entreprene/article/details/146704881?sharetype=blogdetail&sharerId=146704881&sharerefer=PC&sharesource=Small_entreprene&sharefrom=mp_from_link我們學習了線程的控制及其相關概念之后,我們清楚:線程是共享地址空間的,所以線程會共享大部分資源。對于多線程來說,訪問的共享資源稱為共功資源,而多執行流訪問公共資源的時候,可能會造成多種情況的數據不一致問題,因為公共資源并沒有加保護,為了解決這樣的問題,我們就下來就要學習同步與互斥:

互斥話題

在當前學習進程間通信中的信號量的時候,我們有談及,現在我們來快速看看什么是互斥,互斥的相關概念:

進程與線程(執行流)互斥機制的基本概念:

  1. 臨界資源:指在多線程執行過程中,被多個線程共享的資源。(可以理解為被保護起來的共享資源)

  2. 臨界區:指線程內部用于訪問臨界資源的代碼段。

  3. 互斥:確保在任何給定時刻,只有一個執行線程能夠進入臨界區,從而對臨界資源進行訪問,這通常用于保護臨界資源。

  4. 原子性:指一個操作在執行過程中不會被任何調度機制中斷,該操作要么完全執行,要么完全不執行。(后續將討論如何實現原子性)

看一個現象

我們下面來見見一種現象(除了多執行流往顯示器文件上打印這個搶占臨界資源的現象外,另外一種數據不一致問題),然后快速的使用鎖(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. 判斷

但是在執行載入之后,還沒有執行下一條匯編語句的時候,線程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個線程,那么,我們來好好談談這“更多的切換”

我們知道,線程切換其實就是對當前線程的上下文數據,線程上下文切換通常由以下幾種情況觸發:

  1. 時間片到期:操作系統為每個線程分配一個時間片(Time Quantum),當線程運行的時間達到分配的時間片時,操作系統會強制切換到其他線程。

  2. 線程阻塞:線程在等待某些資源(如 I/O 操作、鎖等)時會進入阻塞狀態,操作系統會切換到其他就緒的線程。

  3. 線程優先級調整:操作系統根據線程的優先級動態調整線程的調度順序,高優先級的線程可能會中斷低優先級的線程。

  4. 線程主動讓出 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 時退出循環,它會持有鎖但沒有釋放鎖,導致其他線程無法獲取鎖,從而引發死鎖。

代碼風格問題:解鎖位置不清晰

  • 解鎖操作應該與加鎖操作對稱,即在加鎖的代碼塊結束時進行解鎖。在當前代碼中,解鎖操作被放置在了錯誤的位置,導致代碼邏輯不清晰,容易引發錯誤。

  • 正確的做法是將解鎖操作放在加鎖代碼塊的末尾,確保無論是否進入 ifelse 分支,鎖都能被正確釋放。(當然也可以在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)只是其中一種常見的類型。我們下面對于互斥目標,主要圍繞互斥鎖來進行理解。

硬件級實現(只有內核在用):關閉時鐘中斷

對于一個代碼塊(不是一條匯編),這代碼塊可以隨時被切換,切換是因為時間片到了,操作系統會一直調度線程,一直在做中斷,一直檢測線程的時間片,一旦切換,代碼就發生交叉了,所以鎖的實現的硬件級有一個最簡單粗暴的做法:關閉時鐘中斷。

關閉時鐘中斷的原理

關閉時鐘中斷的基本思想是:

  1. 關閉中斷:在進入臨界區之前,關閉時鐘中斷,這樣當前線程不會被搶占,從而確保臨界區代碼不會被其他線程中斷。

  2. 執行臨界區代碼:在沒有中斷的情況下,安全地執行臨界區代碼。

  3. 打開中斷:臨界區代碼執行完畢后,重新打開中斷,恢復正常的線程調度。

這種方法的優點是簡單直接,但缺點也非常明顯:

  • 風險高:如果臨界區代碼執行時間過長,或者發生死循環,會導致系統無法響應中斷,從而導致系統死機。

  • 僅適用于單核處理器:在多核處理器中,關閉中斷無法阻止其他核心上的線程訪問共享資源。

現代操作系統中的鎖實現

現代操作系統和多核處理器環境中,鎖的實現主要依賴于硬件級的原子操作(如 compare-and-swaptest-and-set),而不是關閉中斷。這些原子操作由處理器直接支持,確保在多核環境中,對共享變量的操作是原子的。

軟件實現(大多使用的鎖,并不簡單粗暴,使用交換)

為了實現互斥鎖操作,大多數體系結構都提供了 swapexchange 指令。該指令的作用是把寄存器和內存單元的數據相交換。由于只有一條指令,保證了原子性。即使是多處理器平臺,訪問內存的總線周期也有先后。一個處理器上的交換指令執行時,另一個處理器的交換指令只能等待總線周期。

下面就是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;
}

?

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

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

相關文章

[Linux系統編程]進程信號

進程信號 1. 信號入門1.1 信號基本概念1.2 技術應用角度的信號2. 信號的產生2.1 通過終端按鍵(如鍵盤)產生信號2.2 通過異常產生信號2.3 調用系統函數向進程發信號2.4 由軟件條件產生信號2.5 總結3. 阻塞信號3.1 信號其他相關常見概念3.2 內核中的信號表示3.3 sigset_t3.3.1 …

要素的選擇與轉出

1.要素選擇的三種方式 當要在已有的數據中選擇部分要素時&#xff0c;ArcMap提供了三種方式:按屬性選擇、位置選擇及按圖形選擇。 1)按屬性選擇 通過設置 SQL查詢表達式&#xff0c;用來選擇與選擇條件匹配的要素。 (1)單擊主菜單下【選擇】【按屬性選擇】&#xff0c;打開【按…

Springboot + Vue + WebSocket + Notification實現消息推送功能

實現功能 基于Springboot與Vue架構&#xff0c;首先使用Websocket實現頻道訂閱&#xff0c;在實現點對點與群發功能后&#xff0c;在前端調用windows自帶的消息通知&#xff0c;實現推送功能。 開發環境 Springboot 2.6.7vue 2.6.11socket-client 1.0.0 準備工作 在 Vue.js…

云手機如何防止設備指紋被篡改

云手機如何防止設備指紋被篡改 云手機作為虛擬化設備&#xff0c;其設備指紋的防篡改能力直接關系到賬戶安全、反欺詐和隱私保護。以下以亞矩陣云手機為例&#xff0c;講解云手機防止設備指紋被篡改的核心技術及實現方式&#xff1a; 系統層加固&#xff1a;硬件級安全防護 1…

有人DTU使用MQTT協議控制Modbus協議的下位機-含數據庫

本文為備忘錄&#xff0c;不做太多解釋。 DTU型號&#xff1a;G780 服務器&#xff1a;win2018 一。DTU設置 正確設置波特率&#xff0c;進入配置狀態&#xff0c;獲取當前參數&#xff0c;修改參數&#xff0c;設置并保存所有參數。 1.通道1設置 2.Modbus輪詢設置 二&am…

湖北師范大學計信學院研究生課程《工程倫理》9.6章節練習

以下是圖片中識別出的文字內容: 1【單選題】當工程師發現所在的企業或公司進行的工程活動會對環境、社會和公眾的人身安全產生危害時,應該及時地給予反映或揭發。這屬于工程師的( ) A、職業倫理責任 B、社會倫理責任 C、個人倫理責任 D、法律責任 2【單選題】下列哪個不屬于工…

Axure RP 9 詳細圖文安裝流程(附安裝包)教程包含下載、安裝、漢化、授權

文章目錄 前言一、Axure RP 9介紹二、Axure RP 9 安裝流程1. Axure RP 9 下載2. 啟動安裝程序3. 安裝向導操作4.完成安裝 三、Axure RP 9 漢化四、Axure RP 9授權 前言 本基礎安裝流程教程&#xff0c;將以清晰、詳盡且易于遵循的步驟介紹Axure RP 9 詳細圖文安裝流程&#xf…

SpringBoot全局exception處理最佳實踐

目錄 自定義異常類 拋出異常 全局異常處理器 自定義異常類 通常會繼承 Exception 或其子類(如 RuntimeException)來定義業務異常類,用于封裝業務相關的錯誤信息。一般選擇繼承 RuntimeException,因為它是一個非受檢異常,在方法中拋出時不需要顯式聲明。 // 自定義業…

node ---- 解決錯誤【Error: error:0308010C:digital envelope routines::unsupported】

1. 報錯 在 Node.js 18.18.0 的版本中&#xff0c;遇到以下錯誤&#xff1a; this[kHandle] new _Hash(algorithm, xofLen);^ Error: error:0308010C:digital envelope routines::unsupported這個錯誤通常發生在運行項目或構建時&#xff0c;尤其是在使用 Webpack、Vite 或其他…

浙江大學鄭小林教授解讀智能金融與AI的未來|附PPT下載方法

導 讀INTRODUCTION 隨著人工智能技術的飛速發展&#xff0c;智能金融已成為金融行業的重要變革力量。浙江大學人工智能研究所的鄭小林教授在2025年3月24日的《智能金融&#xff1a;AI驅動的金融變革》講座中&#xff0c;深入探討了新一代人工智能在金融領域的應用及未來展望。 …

如何實現瀏覽器中的報表打印

在瀏覽器中實現打印一個報表&#xff0c;可以通過以下幾種方法來完成。這里介紹一個基本的流程和相關代碼示例&#xff1a; 1. 使用 JavaScript 的 window.print() 方法 這是最簡單的方法&#xff0c;它會打開打印對話框&#xff0c;讓用戶選擇打印選項。 示例代碼&#xff1…

Linux系統調用編程

進程和線程 進程是操作系統資源分配的基本單位&#xff0c;擁有獨立的地址空間、內存、文件描述符等資源&#xff0c;進程間相互隔離。每個進程由程序代碼、數據段和進程控制塊&#xff08;PCB&#xff09;組成&#xff0c;PCB記錄了進程狀態、資源分配等信息。 線程是…

【力扣hot100題】(054)全排列

挺經典的回溯題的。 class Solution { public:vector<vector<int>> result;void recursion(vector<int>& nums,vector<int>& now){if(nums.size()0){result.push_back(now);return ;}for(int i0;i<nums.size();i){now.push_back(nums[i]);…

【Ragflow】11. 文件解析流程分析/批量解析實現

概述 本文繼續對ragflow文檔解析部分進行分析&#xff0c;并通過腳本的方式實現對文件的批量上傳解析。 文件解析流程 文件解析的請求處理流程大致如下&#xff1a; 1.前端上傳文件&#xff0c;通過v1/document/run接口&#xff0c;發起文件解析請求 2.后端api\apps\docum…

2024年零知識證明(ZK)研究進展

Sumcheck 整個領域正在轉向更多地依賴于 Sumcheck Protocol Sumcheck是用于驗證多項式承諾的協議,常用于零知識證明(ZKP)中,尤其是在可驗證計算和擴展性上。它的主要目的是通過對多項式進行分段檢查,從而保證某個多項式在給定輸入上的正確性,而不需要直接計算出整個多項…

thinkphp每條一級欄目中可自定義添加多條二級欄目,每條二級欄目包含多個字段信息

小程序客戶端需要展示團購詳情這種結構的內容,后臺會新增多條套餐,每條套餐可以新增多條菜品信息,每條菜品信息包含菜品名稱,價格,份數等字段信息,類似于購物網的商品多規格屬性,數據表中以json類型存儲,手寫了一個后臺添加和編輯的demo 添加頁面 編輯頁面(json數據…

Vue3引入ElementPlus

1.ElementPlus屬于第三方的應用框架&#xff0c;官網地址&#xff1a;設計 | Element Plus &#xff0c;學習可以參考該網站的指南。 2.安裝element-plus &#xff0c;指令為&#xff1a;npm install element-plus --save 3.引入elementplus的全局&#xff0c;組件、樣式、圖標…

react+antd封裝一個可回車自定義option的select并且與某些內容相互禁用

需求背景 一個select框 現在要求可多選 并且原有一個any的選項 其他選項為輸入后回車自己增加 若選擇了any 則其他選項不可選擇反之選擇其他選項any不可選擇 并且回車新增時也不可直接加入到選中數組只加入到option內 并且不可重復添加新內容 實現過程 <Form.Item …

Oracle數據庫數據編程SQL<8 文本編輯器Notepad++和UltraEdit(UE)對比>

首先&#xff0c;用戶界面方面。Notepad是開源的&#xff0c;界面看起來比較簡潔&#xff0c;可能更適合喜歡輕量級工具的用戶。而UltraEdit作為商業軟件&#xff0c;界面可能更現代化&#xff0c;功能布局更復雜一些。不過&#xff0c;UltraEdit支持更多的主題和自定義選項&am…

【學Rust寫CAD】30 Alpha256結構體補充方法(alpha256.rs)

源碼 impl Alpha256 {#[inline]pub fn alpha_mul(&self, x: u32) -> u32 {let mask 0xFF00FF;let src_rb ((x & mask) * self.0) >> 8;let src_ag ((x >> 8) & mask) * self.0;(src_rb & mask) | (src_ag & !mask)} }代碼分析 功能 輸…