個人主頁:chian-ocean
文章專欄-Linux
前言:
互斥是并發編程中避免競爭條件和保護共享資源的核心技術。通過使用鎖或信號量等機制,能夠確保多線程或多進程環境下對共享資源的安全訪問,避免數據不一致、死鎖等問題。
競爭條件
競爭條件(Race Condition)是并發程序設計中的一個問題,指在多個線程或進程并發執行時,由于它們對共享資源的訪問順序不確定,可能導致程序的輸出或行為依賴于執行的順序,從而產生不一致或不可預測的結果。
例如:一個假脫機打印程序。當一個進程需要打印一個文件時,它將文件名放在一個特殊的假脫機目錄**(spoalerdirectory)**下。另一個進程(打印機守護進程)則周期性地檢查是否有文件需要打印,若有就打印并將該文件名從目錄下刪掉)
-
理想情況:
- 設想假脫機目錄中有許多槽位,編號依次為
0,1,2,……
,每個槽位存放一個文件名。 - 同時假設有兩個共享變量:
out
,指向下一個要打印的文件:in
,指向目錄中下一個空閑槽位。 - 可以把這兩個變量保存在一個所有進程都
out=4
進程Ain=7
能訪問的文件中,該文件的長度為兩個字。 - 在某一時刻,進程B號至3號槽位空(其中的文件已經打印完畢),4號至6號槽位被占用(其中存有排好隊列的要打印的文件名)。幾乎在同時刻,進程A和進程B都決定將一個文件排隊打印,這種情圖兩個進程同時想訪問共享內存
- 設想假脫機目錄中有許多槽位,編號依次為
-
實際情況:
- 進程A讀到
in
的值為7
,將7
存在一個局部變量next_free_slot
中。 - 此時發生一次時鐘中斷,CPU認為進程A已運行了足夠長的時間,決定切換到進程B。進程B也讀取
in
,同樣得到值為7
,于是將7
存在B的局部變量next_free_slot
中。 - 在這一時刻兩個進程都認為下一個可用槽位是
7
.進程B現在繼續運行,它將其文件名存在槽位7中并將in
的值更新為8
。然后它離開,繼續執行其他操作最后進程A接著從上次中斷的地方再次運行。 - 它檢查變量
next_free_slot
,發現其值為7,于是將打印文作名存人7號槽位,這樣就把進程B存在那里的文件名覆蓋掉。然后它將next_free_slot
加1,得到值為8,就將8存到in
中。 - 此時,假脫機目錄內部是一致的,所以打印機守護進程發現不了任何錯誤,但進程B卻永遠得不到任何打印輸出。類似這樣的情況,即兩個或多個進程讀寫某些共享數據,而最后的結果取決于進程運行的精確時序,稱為競爭條件(race condition)。
- 進程A讀到
實際搶票問題:
#include<iostream>
#include<unistd.h>
#include<pthread.h>using namespace std;#define NUM 10 // 定義線程數量,這里創建 10 個線程
int ticket = 1000; // 票數從 1000 開始// 線程執行的函數
void* mythread(void* args)
{pthread_detach(pthread_self()); // 分離線程,線程結束后自動釋放資源uint64_t number = (uint64_t)args; // 將傳入的參數(線程編號)轉換為 uint64_t 類型while(true){if(ticket > 0) // 如果還有票{usleep(1000); // 模擬一些延遲,減少系統負載cout <<"thread: " << number << " ticket: " << ticket << endl; // 打印線程編號和剩余票數ticket--; // 減少票數}else {break; // 如果沒有票了,退出循環}usleep(20); // 再次暫停 20 微秒,模擬其他操作}return nullptr; // 線程結束時返回空指針
}int main()
{// 創建 NUM 個線程for(int i = 0; i < NUM; i++){pthread_t tid;pthread_create(&tid,nullptr,mythread,(void*)i); // 創建線程,傳入線程編號}sleep(5); // 主線程等待 5 秒,確保子線程有足夠的時間執行cout <<"process quit ..." <<endl; // 打印主線程退出消息return 0;
}
簡單描述:
- 線程數量和票數:
- 定義了一個全局變量
ticket
,初始值為 1000,表示共有 1000 張票。 - 程序創建了 10 個線程(
NUM = 10
),每個線程將嘗試減少ticket
的值,模擬每個線程購買一張票。
- 定義了一個全局變量
- 線程函數:
- 每個線程執行
mythread
函數,函數內部通過一個while
循環不斷檢查ticket
是否大于 0。如果ticket
大于 0,則線程會輸出剩余票數并減去一張票,模擬賣票操作。 - 使用
usleep(1000)
模擬了一個小延遲,避免線程占用過多 CPU 資源,并且增加了另一個小的usleep(20)
讓線程執行有一定的間隔。
- 每個線程執行
- 主線程:
- 主線程創建了 10 個線程,并且等待 5 秒后退出,給子線程一些時間執行任務。
潛在問題:
- 競態條件(Race Condition):
- 問題描述:多個線程同時訪問并修改共享資源
ticket
,可能會發生競態條件。由于ticket--
操作并不是原子的(即分為讀取、修改和寫入三步),多個線程在同一時間訪問ticket
時,可能會同時讀取到相同的值并同時更新,導致票數沒有正確減少,可能會出現賣出同一張票的情況。 - 解決方案:可以通過互斥鎖(
pthread_mutex_t
)來保證每次只有一個線程能修改ticket
,避免并發寫入導致的錯誤。
- 問題描述:多個線程同時訪問并修改共享資源
臨界區
臨界區(Critical Section) 是指在多線程或多進程程序中,共享資源被多個線程或進程同時訪問和修改的代碼區域。為了確保共享資源在多線程或多進程環境中的一致性和正確性,我們需要對訪問臨界區的操作進行同步控制,以避免發生競爭條件(Race Condition)。
臨界區的特點:
- 共享資源訪問:臨界區中的代碼通常會訪問共享資源,例如共享內存、文件、全局變量、硬件資源等。
- 并發執行:多個線程或進程可能同時嘗試進入臨界區,并對共享資源進行修改。
- 資源競爭:如果多個線程/進程在同一時刻進入臨界區并修改共享資源,就可能導致數據沖突、不一致或錯誤。
臨界區的問題:
- 數據一致性問題:多個線程或進程同時修改共享數據,可能導致數據不一致、錯誤或丟失。
- 資源沖突:當多個線程或進程試圖同時訪問共享資源時,可能會引發系統資源競爭,影響程序的正確性和效率。
解決方案
- 互斥鎖(Mutex): 互斥鎖用于確保在某一時刻只有一個線程能夠訪問臨界區。當一個線程需要進入臨界區時,它會獲取互斥鎖,其他線程必須等待該線程釋放鎖后才能進入臨界區。
- 信號量(Semaphore): 信號量可以控制對共享資源的并發訪問。通過限制允許訪問臨界區的線程數量,可以避免過多的線程同時進入臨界區。
這樣盡管可以避免競爭條件,但是這樣不能保證共享數據進行正確高效的協作,還要滿足以下4個條件:
- 任何兩個進程不能同時處于臨界區。
- 不應該對CPU的數量和速度進行任何假設。
- 臨界區外的進程不得阻塞其他進程。
- 不得使進程無期限等待進入臨界區。
臨界區的優化:
- 減少臨界區的長度:盡量將臨界區的代碼量減少到最小,避免過長時間占用臨界區。
- 避免不必要的鎖:對于只讀的共享資源,盡量避免加鎖,減少鎖帶來的性能開銷。
- 使用無鎖編程(Lock-Free Programming):通過原子操作(如
atomic
類型)和 CAS(Compare-And-Swap)等無鎖技術,避免傳統鎖機制帶來的性能瓶頸。
互斥鎖
互斥鎖(Mutex) 是一種用于多線程編程的同步機制,旨在防止多個線程同時訪問和修改共享資源,從而確保數據的一致性和程序的正確性。
互斥鎖初始化
- 全局域初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 使用默認屬性初始化
- 局部區初始化
pthread_mutex_t mutex; // 定義一個互斥鎖變量pthread_mutex_init(&mutex, NULL); // 初始化互斥鎖。NULL表示使用默認的屬性pthread_mutex_destroy(&mutex); // 銷毀互斥鎖,在不再使用鎖時調用
加鎖、解鎖
-
pthread_mutex_lock:用于鎖定一個互斥鎖。若互斥鎖已被其他線程鎖定,則調用線程會阻塞,直到互斥鎖被釋放。
-
pthread_mutex_trylock:嘗試鎖定互斥鎖。與
pthread_mutex_lock
不同的是,它不會阻塞線程。如果鎖定成功,返回 0;如果鎖定失敗(即鎖已經被其他線程持有),則返回一個非零值。 -
pthread_mutex_unlock:用于解鎖一個已鎖定的互斥鎖。如果當前線程沒有持有該鎖,調用此函數將導致未定義的行為。
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL); // 初始化互斥鎖pthread_mutex_lock(&mutex); // 鎖定互斥鎖
// 訪問共享資源
pthread_mutex_unlock(&mutex); // 解鎖互斥鎖
優化搶票問題
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 定義并初始化一個互斥鎖
#define NUM 10 // 定義創建的線程數量
int ticket = 1000; // 定義一個全局變量 ticket,初始為 1000,表示票的數量// 線程函數,用于模擬每個線程購買票
void* mythread(void* args)
{pthread_detach(pthread_self()); // 將當前線程設置為分離線程,結束后自動回收資源uint64_t number = (uint64_t)args; // 將傳入的參數(線程編號)轉換為 uint64_t 類型while(true) // 循環,直到票數為 0{{pthread_mutex_lock(&lock); // 鎖定互斥鎖,確保對 ticket 資源的互斥訪問if(ticket > 0) // 如果還有票{usleep(1000); // 模擬工作延遲,單位為微秒(1 毫秒)cout <<"thread: " << number << " ticket: " << ticket << endl; // 輸出當前線程編號和剩余票數ticket--; // 票數減少}else // 如果票數為 0,退出循環{break;}pthread_mutex_unlock(&lock); // 解鎖,允許其他線程訪問 ticket 資源}}return nullptr; // 返回空指針,結束線程
}int main()
{// 創建多個線程for(int i = 0; i < NUM; i++) // 創建 NUM 個線程{pthread_t tid; // 定義線程 IDpthread_create(&tid, nullptr, mythread, (void*)i); // 創建線程并傳遞參數(線程編號)}sleep(5); // 主線程休眠 5 秒,確保所有線程執行一段時間cout <<"process quit ..." <<endl; // 輸出退出信息,表示主進程結束return 0; // 返回 0,程序結束
}
代碼詳細注釋解析:
-
全局變量:
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
:定義了一個全局互斥鎖,初始化時就已經可用。這個鎖是為了防止多個線程同時訪問和修改ticket
變量導致的并發問題。#define NUM 10
:定義了一個宏NUM
,表示需要創建的線程數量(此處為 10)。int ticket = 1000;
:全局變量ticket
表示剩余票數,初始值為 1000。
-
線程函數
mythread
:-
pthread_detach(pthread_self());
:將當前線程設置為分離線程,這樣線程結束時系統會自動回收資源,無需顯式調用pthread_join
來等待線程結束。 -
uint64_t number = (uint64_t)args;
:將傳遞給線程函數的參數(線程編號)轉換為uint64_t
類型,以便進行打印。 -
在
while(true)
循環中,線程將不斷檢查ticket
是否大于 0:
- 使用
pthread_mutex_lock(&lock);
上鎖,防止多個線程同時修改ticket
變量,保證每次只有一個線程能訪問和修改票數。 - 如果
ticket > 0
,則輸出當前線程的編號和剩余票數,并將票數減 1。每次操作后調用usleep(1000);
來模擬工作延時。 - 如果
ticket
為 0,跳出循環。 - 最后,通過
pthread_mutex_unlock(&lock);
解鎖,允許其他線程訪問共享資源。
- 使用
-
-
主函數
main
:for
循環中,創建了 10 個線程,每個線程都會執行mythread
函數。線程編號(i
)被傳遞到每個線程中,作為其唯一標識。sleep(5);
:主線程休眠 5 秒,以確保創建的 10 個子線程有足夠的時間執行完畢。cout <<"process quit ..." <<endl;
:輸出程序退出信息,表示主程序結束。
打印:
互斥鎖封裝(RAII)
class mutex
{
public:
private:pthread_mutex_t * _mutex; // 互斥鎖指針public:mutex(pthread_mutex_t* mutex):_mutex(mutex) // 構造函數,初始化互斥鎖指針{//pthread_mutex_init(_mutex,nullptr); // 互斥鎖的初始化被注釋掉了}void lock(){pthread_mutex_lock(_mutex); // 鎖定互斥鎖}void unlock(){pthread_mutex_unlock(_mutex); // 解鎖互斥鎖}~mutex() {} // 析構函數,什么都不做
};class Guard
{
private:mutex _lock; // 使用上面定義的 mutex 類來管理鎖public:Guard(pthread_mutex_t* lock):_lock(lock) // 構造函數中鎖定互斥鎖{_lock.lock(); // 自動鎖定}~Guard() // 析構函數中解鎖{_lock.unlock(); // 自動解鎖}
};
這段代碼的設計實現了一個典型的 RAII(資源獲取即初始化) 模式,尤其是在 Guard
類中得到了完美的體現。RAII 是 C++ 中管理資源(如內存、文件句柄、互斥鎖等)的一種設計模式。在該模式下,資源在對象的構造函數中獲取,在對象的析構函數中釋放,這樣可以確保即使發生異常,也能正確釋放資源,避免資源泄漏和死鎖。
// 構造函數中鎖定互斥鎖
{
_lock.lock(); // 自動鎖定
}
~Guard() // 析構函數中解鎖
{_lock.unlock(); // 自動解鎖
}
};
這段代碼的設計實現了一個典型的 **RAII(資源獲取即初始化)** 模式,尤其是在 `Guard` 類中得到了完美的體現。RAII 是 C++ 中管理資源(如內存、文件句柄、互斥鎖等)的一種設計模式。在該模式下,資源在對象的構造函數中獲取,在對象的析構函數中釋放,這樣可以確保即使發生異常,也能正確釋放資源,避免資源泄漏和死鎖。