🤖個人主頁:晚風相伴-CSDN博客
💖如果覺得內容對你有幫助的話,還請給博主一鍵三連(點贊💜、收藏🧡、關注💚)吧
🙏如果內容有誤或者有寫的不好的地方的話,還望指出,謝謝!!!
讓我們共同進步
?
下一篇《生產者消費者模型》敬請期待
目錄
🔥線程間互斥的相關概念
💪互斥量的接口
初始化互斥量
銷毀互斥量
互斥量的加鎖與解鎖
🔥探究互斥量實現原理
可重入函數和線程安全?
兩者的概念區分?
常見的線程不安全和安全情況
可重入與線程安全的聯系與區別
?死鎖?
產生死鎖的四個必要條件
避免死鎖
🔥線程同步?
條件變量?
同步的概念與競態條件
🔥條件變量接口
初始化?
銷毀條件?
條件等待
喚醒等待
🔥解釋pthread_cond_wait中的互斥量
🔥線程間互斥的相關概念
- 臨界資源:多線程執行流共享的資源就叫做臨界資源
- 臨界區:每個線程內部,訪問臨界資源的代碼,就叫做臨界區
- 互斥:任何時刻,互斥保證有且只有一個執行流進入臨界區,訪問臨界資源,通常對臨界資源其保護作用
- 原子性:不會被任何調度機制打斷的操作,該操作只有兩種狀態,要么完成,要么未完成。
先來看看下面簡單實現的搶票的代碼
int tickets = 1000;void* getTickets(void* args){(void)args;while(true){if(tickets > 0){usleep(1000);printf("%p: %d\n", pthread_self(), tickets);tickets--;}else{break;}}return nullptr;}int main(){pthread_t t1, t2, t3;pthread_create(&t1, nullptr, getTickets, nullptr);pthread_create(&t1, nullptr, getTickets, nullptr);pthread_create(&t1, nullptr, getTickets, nullptr);pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;}
結果演示
?
💪為什么結果會出現-1呢?
原因:首先要知道一個線程什么時候被調度,調度多長時間,完全是有計算機確定的,程序員決定不了。tickets在進行減減操作時,是分三步的
①讀取數據到CPU內的寄存器中
②CPU內部進行計算--
③將結果寫回內存中
為了方便敘述,這里給線程編個號
一號線程來了,由于時間片很短執行到第②步就被切走了,二號線程來了,它沒有被打斷,所以它執行完了這三步,并且這個線程的優先級比較高,一直執行tickets--操作,直到tickets減到1停止,在執行到第①步的時候被切走了,而一號線程回來了,繼續從它被打斷的地方繼續向后執行,也就是從第②步開始繼續向后執行,在寫回內存后,tickets已經減到了1,但是這個線程又把tickets修改為了999,并且這時它的時間片很長,所以這次又一直將tickets減到了1,由于判斷條件tickets不為0,所以tickets繼續減減操作,此時tickets減為了0,此時二號線程來了,將0讀入到寄存器中進行減減操作,所以結果出現了-1,這就導致了問題的出現。
?
?要解決上面的問題,就需要做到以下三點:
- 代碼必須要有互斥行為:當代碼進入臨界區執行時,不允許其它線程進入臨界區。
- 如果多個線程同時要求執行臨界區的代碼,并且臨界區沒有線程在執行,那么只能允許一個線程進入該臨界區。
- 如果線程不在臨界區中執行,那么該線程不能阻止其它線程進入臨界區。
要做到以上三點,就需要一把互斥鎖,將臨界區資源鎖住,沒有拿到鑰匙的線程就不能訪問臨界區資源,這就能做到保護了臨界區資源。Linux上提供的這把互斥鎖叫互斥量。
?
💪互斥量的接口
初始化互斥量
有兩種方式初始化互斥量
方法一:全局初始化分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:局部初始化分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
參數:
- mutex:要初始化的互斥量
- attr:nullptr
返回值:成功返回0,失敗返回錯誤碼
銷毀互斥量
?int pthread_mutex_destroy(pthread_mutex_t *mutex);
參數:
- mutex:要銷毀的互斥量
返回值:成功返回0,失敗返回錯誤碼
?銷毀互斥量時需要注意
- 使用全局初始化的互斥量不需要銷毀
- 不要銷毀一個已經加鎖的互斥量
- 已經銷毀的互斥量要確保后面的代碼中不再有加鎖的操作
互斥量的加鎖與解鎖
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失敗返回錯誤碼
?調用pthread_mutex_lock加鎖時,可能會遇到以下情況:
- 互斥量還沒被加鎖,處于未鎖定狀態,那么調用該函數會將互斥量加鎖鎖定。
- 在調用該函數之前,其它線程已經申請了鎖,鎖定了該互斥量,或者存在其它線程同時競爭式的申請互斥量,但沒有競爭到互斥量,那么調用pthread_mutex_lock就會被阻塞,等待會吃兩解鎖。
所以將上面的搶票代碼修改如下:
int tickets = 1000; // 臨界資源class ThreadData
{
public:ThreadData(string &name, pthread_mutex_t *pmtx) : _tname(name), _pmtx(pmtx){}public:string _tname;pthread_mutex_t *_pmtx;
};void *getTickets(void *args)
{ThreadData* td = (ThreadData*)args;while (true){int n = pthread_mutex_lock(td->_pmtx); // 加鎖保護臨界區資源assert(n == 0);if (tickets > 0){usleep(1000);printf("%s : %d\n", td->_tname.c_str(), tickets);cout << td->_tname << " : " << tickets << endl;tickets--;n = pthread_mutex_unlock(td->_pmtx);assert(n == 0);}else{n = pthread_mutex_unlock(td->_pmtx);assert(n == 0);break;}// 處理后續的動作cout << "恭喜,搶票成功" << endl;usleep(1000);}return nullptr;
}#define THREAD_NUM 5int main()
{pthread_mutex_t mtx;pthread_mutex_init(&mtx, nullptr); // 局部定義的鎖進行初始化的形式pthread_t tid[THREAD_NUM];for (int i = 0; i < THREAD_NUM; i++){string name = "thread ";name += to_string(i + 1);ThreadData *td = new ThreadData(name, &mtx);pthread_create(tid + i, nullptr, getTickets, (void *)td);}for (int i = 0; i < THREAD_NUM; i++){pthread_join(tid[i], nullptr);}pthread_mutex_destroy(&mtx); // 最后將鎖釋放掉return 0;
}
?結果演示:
?
🔥探究互斥量實現原理
加鎖的目的是保證操作的原子性。?從匯編的角度來看,如果只有一條匯編語句,我們就認為該匯編語句的執行是原子的,?在匯編中給我們提供了swap或者exchange指令,該指令的作用是將內存中的數據與CPU內寄存器中的數據(CPU內寄存器中的數據也叫做執行流的上下文,寄存器的空間是被所有執行流鎖共享的,但是里面的數據是被某一個執行流私有的)進行交換,由于只有一條指令,所以可以保證其原子性。
?
?
解鎖時會把互斥量變為1。
可重入函數和線程安全?
兩者的概念區分?
線程安全:多個線程并發執行同一段代碼時,不會出現不同的結果。
重入:同一個函數被不同的執行流調用,當前一個執行流還沒有執行完,就有其它的執行流再次進入該函數,我們稱這種情況是重入。一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函數被稱為可重入函數,否則稱為不可重入函數。
常見的線程不安全和安全情況
不安全情況:
- 不保護共享變量的函數
- 函數狀態隨著被調用,狀態發生變化的函數
- 返回指向靜態變量指針的函數
- 調用線程不安全函數的函數
?安全情況:
- ?每個線程對全局變量或者靜態變量只有讀取權限,而沒有寫入權限,一般來說這些線程是安全的。
- 類或者接口對于線程來說都是原子操作的
- 多個線程之間的切換不會導致該接口的執行結果存在二義性
可重入與線程安全的聯系與區別
聯系:
- 函數是可重入的,那就是線程安全的。
- 函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題。
- 如果一個函數中有全局變量,那么這個函數既不是線程安全也不是可重入的。
區別:
- 可重入函數是線程安全函數的一種
- 線程安全不一定是可重入的,而可重入函數則一定是線程安全的
- 如果對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個可重入函數的鎖還未釋放則會產生死鎖,因此是不可重入的。
?死鎖?
死鎖是指子在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其它進程所占用不會釋放的資源而處于的一種永久等待狀態。
產生死鎖的四個必要條件
- 互斥條件:一個資源每次只能被一個執行流使用
- 請求與保持條件:一個執行流因請求支援而阻塞時,對已獲得的資源保持不放
- 不剝奪條件:一個執行流已獲得的資源,在未使用完之前,不能被強行剝奪
- 循環等待條件: 若干執行流之間形成一種頭尾相接的循環等待資源的關系
避免死鎖
- 破壞死鎖的四個必要條件
- 加鎖順序一致
- 避免鎖未釋放的場景
- 資源一次性分配
- 對死鎖檢測
- 銀行家算法
🔥線程同步?
條件變量?
當我們申請臨界資源前,要先檢測臨界資源是否存在,做檢測的本質也是在訪問臨界資源,所以對臨界資源的檢測一定是要在加鎖和解鎖之間的。例如一個線程訪問隊列時,發現隊列為空,那么它只能等待,直到其它線程將一個節點添加到隊列中,在檢測隊列是否為空時,如果該線程一直輪詢檢測,那么勢必要頻繁的申請鎖和釋放鎖,這樣太浪費資源了,那么這種情況就需要用到條件變量了。
因此條件變量可以讓線程不在頻繁的自己檢測了,當第一次檢測到條件不滿足時就掛起等待,當條件滿足時,再通知該線程,讓它來申請資源和訪問。
同步的概念與競態條件
同步:在保證數據安全的前提下,讓線程能夠按照某種特定的順序訪問臨界資源,從而有效的解決了訪問臨界資源的合理性問題。
競態條件:因為時序問題,而導致程序異常,我們稱之為競態條件。
🔥條件變量接口
初始化?
和互斥量那里一樣分為全局初始化和局部初始化
局部初始化?
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
參數:
- cond:要初始化的條件變量
- attr:設置為nullptr即可
返回值:成功返回0,失敗返回錯誤碼
全局初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;?
銷毀條件?
int pthread_cond_destroy(pthread_cond_t *cond) ;
條件等待
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
參數:
- cond:要在這個條件變量上等待
- mutex:互斥量
喚醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);//喚醒一批線程
int pthread_cond_signal(pthread_cond_t *cond);//喚醒某個線程
示例代碼
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;#define NUM 4
typedef void (*func_t)(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond);
volatile bool quit = false;class ThreadData
{
public:ThreadData(string &name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond): _name(name), _func(func), _pmtx(pmtx), _pcond(pcond){}public:string _name;func_t _func;pthread_mutex_t *_pmtx;pthread_cond_t *_pcond;
};void func1(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);//線程等待cout << name << " running... -- 1" << endl;// sleep(1);pthread_mutex_unlock(pmtx);}
}void func2(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);//線程等待cout << name << " running... -- 2" << endl;// sleep(1);pthread_mutex_unlock(pmtx);}
}void func3(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);//線程等待cout << name << " running... -- 3" << endl;// sleep(1);pthread_mutex_unlock(pmtx);}
}void func4(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{while(!quit){pthread_mutex_lock(pmtx);pthread_cond_wait(pcond, pmtx);//線程等待cout << name << " running... -- 4" << endl;// sleep(1);pthread_mutex_unlock(pmtx);}
}void* Entry(void* args)
{ThreadData* tmp = (ThreadData*)args;tmp->_func(tmp->_name, tmp->_pmtx, tmp->_pcond);delete tmp;return nullptr;
}int main()
{pthread_mutex_t mtx;pthread_cond_t cond;pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);pthread_t tid[NUM];func_t funcs[NUM] = {func1, func2, func3, func4};for (int i = 0; i < NUM; i++){string name = "thread ";name += to_string(i + 1);ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);pthread_create(tid + i, nullptr, Entry, (void*)td);}int cnt = 10;while(cnt){cout << "resume thread run code..." << cnt-- << endl;pthread_cond_signal(&cond);// pthread_cond_broadcast(&cond);sleep(1);}cout << "ctrl done" << endl;quit = true;pthread_cond_broadcast(&cond);for(int i = 0; i < NUM; i++){pthread_join(tid[i], nullptr);cout << "pthread: " << tid[i] << " quit" << endl; }pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}
結果演示
??
按照一定的順序執行。
🔥解釋pthread_cond_wait中的互斥量
條件等待是線程間同步的一種手段,如果只有一個線程,條件不滿足,一直等下去也都不會滿足,所以必須還要有一個線程通過某些操作來改變共享變量,使得不滿足的條件變得滿足,并且友好的通知在條件變量上等待的線程。但是條件不會無緣無故的滿足,這必然會牽扯到共享數據的改變。共享數據屬于臨界資源,因此一定要用互斥鎖來保護,沒有互斥鎖的保護就無法安全的獲取和修改共享數據了。
?
按照上面的說法,我們轉換成代碼,必須先上鎖,檢測到條件不滿足時,pthread_cond_wait會解鎖,然后在條件變量上等待,直到條件滿足時,pthread_cond_wait又會重新加鎖。
進入pthread_cond_wait函數后,會去檢測條件是否滿足,如果不滿足就把互斥量變為1(解鎖),直到條件滿足后(pthread_cond_wait返回)將互斥量恢復成原樣。
條件變量的規范使用如下:
//等待條件代碼
pthread_mutex_lock(&mtx);
while(條件檢測)pthread_cond_wait(&cond, &mtx);
//修改條件
pthread_mutex_unlock(&mtx);//條件滿足,喚醒線程代碼
pthread_mutex_lock(&mtx);
//設置條件滿足
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);
??