文章目錄
- 1. 互斥
- 1.1 為什么需要互斥
- 1.2 互斥鎖
- 1.3 初談互斥與同步
- 1.4 鎖的原理
- 1.5 可重入VS線程安全
- 1.6 死鎖
- 1.7 避免死鎖的算法(擴展)
- 序:在上一章中我們知道了線程控制的三個角度:線程創建、線程等待和線程終止,分別從接口以及參數的意義和功能的角度來了解,以及最后深入原生線程庫,了解用戶級線程與內核輕量型進程的關系。而本章將從線程的同步與互斥的角度來帶大家了解什么是互斥和同步,以及為什么要互斥和同步等一系列問題。
上一章線程控制的知識補充:
線程分離:
pthread_detach函數,可以是線程組內其他線程對目標線程進行分離,也可以是線程自己分離,分離后的線程不可被等待,如果強行等待也會返回錯誤碼22。
問題一:為什么要有線程分離呢?
如果不關心線程的返回值,join是一種負擔,這個時候,我們可以告訴系統,當線程退出時,自動釋放線程資源。
歸根結底,我們讓線程分離,其實就是更改線程的原生線程庫里的tcb內的分離的屬性,而pthread_join就是識別到了該分離屬性被更改為已分離,所以才會直接返回一個錯誤碼。
1. 互斥
1.1 為什么需要互斥
多線程搶票模型代碼演示:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
#include<vector>
using namespace std;#define NUM 4int ticket =100;//用多線程,模擬一輪搶票class ThreadData
{
public:ThreadData(int number){_thread_name ="thread-" + to_string(number);}
public:string _thread_name;
};void* GetTicket(void* args)
{ThreadData* td=static_cast<ThreadData*>(args);const char* name =td->_thread_name.c_str();while(true){if(ticket>0){usleep(5000);printf("i am %s,get a ticket:%d\n",name,ticket);ticket--;}else break;}printf("%s ... quit\n",name);return nullptr;}
int main()
{vector<pthread_t> tids;vector<ThreadData*> thread_datas;for(int i=0;i<NUM;i++){pthread_t tid;ThreadData* td=new ThreadData(i);thread_datas.push_back(td);pthread_create(&tid,nullptr,GetTicket,thread_datas[i]);tids.push_back(tid);}for(auto &e :tids){pthread_join(e,nullptr);}for(auto &e :thread_datas){delete e;}return 0;
}
結果如圖:
那么問題來了搶票模型中,為什么搶票搶到最后,竟然搶到了負數?這個問題我們暫且不談,我們繼續往下說。
要想了解為什么會出現這樣的情況,我們首先就要知道既然ticket出現了負數,就說明ticket–出現了問題,共享數據------>數據不一致問題!!!(肯定和多線程并發訪問是有關系的),對一個全局變量進行多線程并發–或++操作是否是安全的?所以這個–操作不是原子的,所以也不是安全的。
既然–操作是不安全的,不是原子的,那我們要了解ticket–究竟要有哪些步驟:
第一個步驟:先將ticket讀入到CPU的寄存器當中
第二個步驟:CPU內部進行–操作
第三個步驟:將計算結果寫回內存
但是想要了解為什么會出現這樣的情況,我們還要了解一個額外的知識點:寄存器不等于寄存器的內容線程在執行的時候,將共享數據,加載到CPU寄存器的本質:把數據的內容,變成了自己的上下文 — 這樣的數據以拷貝的形式給自己單獨拿了一份
既然ticket–是有三個步驟組成,如果在這三個步驟之內發生了線程切換就會導致數據不一致的問題!!!
假設當前搶票的進程中,票有1000張,該進程內有兩個線程正在搶票,此時thread-1線程正在實現搶票,剛完成第一步,將內存中的數據讀入寄存器中,也就是讀入該線程的上下文中,如果此時來了第二個線程thread-2也要實行搶票,將thread-1線程切換了,thread-1線程就會帶著這個1000的數據一起離開,等待線程再次切換回來!!!
當線程切換到thread-2線程,假設此時thread-2線程在下一次線程切換的時間片內進行了100次搶票的動作,此時的票數就由1000變成了900
當thread-2線程搶了100張票后,將寄存中的900寫回給內存中的ticket,此時thread-1線程切換回來了,thread-2線程就要帶著自己的硬件上下文走,于是thread-2就將寄存器中的900帶走了,thread-1線程切換回來后,會將之前就帶走的寄存器中的1000帶回來,在放入到寄存器中,即恢復上下文。
然后,此時thread-1會繼續執行未完成的動作,繼續執行第二步和第三步。
這就會導致,thread-2辛辛苦苦搶的票,將1000變成900,結果thread-1線程切換回來后就變成了999。
現在讓我們來回答最開始的那個問題,為什么ticket會出現負數?
假設此時的實際票數小于線程數,此時有四個線程,但票只有兩個了,別忘了ticket>0也是運算(邏輯運算)所以此時同時有4個線程都對ticket進行了邏輯運算,此時票有兩個,都是大于0,此時四個線程都進入了該循環內,都可以進行ticket–,這也就是為什么會出現了負數!!!
1.2 互斥鎖
怎么解決上述的一些列問題???
對共享數據的任何訪問,保證任何時候只有一個執行流進行訪問!—互斥!!!
而想要實現互斥就要引入互斥鎖的概念!!!
鎖資源的定義,初始化和釋放:
pthread_mutex_t是庫提供的一種數據類型
pthread_mutex_init(第一個參數傳鎖,第二個參數傳鎖的各種參數(默認傳nullptr))
pthread_mutex_destroy
(如果用pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALZER進行全局變量的初始化,就不用調用pthread_mutex_destroy函數進行釋放)
一種臨界資源,由多個線程訪問,如果想要保證臨界資源的安全,就必須讓這個多個線程訪問同一把鎖!!!
鎖的申請和釋放:
pthread_mutex_lock
pthread_mutex_unlock
其中的pthread_mutex_trylock函數就是加鎖的非阻塞版本
到這一步,大家只能對鎖有個印象,沒法深刻知道鎖的作用和鎖的使用,接下來我將改進原本的多線程搶票模型的代碼,用互斥鎖來使原版中的多線程導致的數據不一致問題得到解決!!!
互斥鎖版的多線程搶票模型代碼演示
#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
#include<vector>using namespace std;#define NUM 5int ticket =100;//用多線程,模擬一輪搶票class ThreadData
{
public:ThreadData(int number,pthread_mutex_t* lock){_thread_name ="thread-" + to_string(number);_lock=lock;}
public:string _thread_name;pthread_mutex_t *_lock;
};void* GetTicket(void* args)
{ThreadData* td=static_cast<ThreadData*>(args);const char* name =td->_thread_name.c_str();while(true){pthread_mutex_lock(td->_lock);if(ticket > 0){//usleep(5000);printf("i am %s,get a ticket:%d\n",name,ticket);ticket--;}else{pthread_mutex_unlock(td->_lock);break;}pthread_mutex_unlock(td->_lock);usleep(5000);}printf("%s ... quit\n",name);return nullptr;
}int main()
{pthread_mutex_t lock;pthread_mutex_init(&lock,nullptr);vector<pthread_t> tids;vector<ThreadData*> thread_datas;for(int i=0;i<NUM;i++){pthread_t tid;ThreadData* td=new ThreadData(i,&lock);thread_datas.push_back(td);pthread_create(&tid,nullptr,GetTicket,thread_datas[i]);tids.push_back(tid);}for(auto &e :tids){pthread_join(e,nullptr);}for(auto &e :thread_datas){delete e;}pthread_mutex_destroy(&lock);return 0;
}
對于上面一段代碼,我們用鎖將臨界資源鎖住,同一時間只能有一個線程進行訪問,從而實現對臨界資源的保護
其中,被鎖保護的資源叫做臨界資源,某幾段訪問臨界資源的代碼區叫做臨界區
加鎖的本質:是用時間來換安全
加鎖的表現:線程對臨界區代碼的串行執行
加鎖原則:盡量保證臨界區代碼越少越好。
申請鎖成功了,才能往后執行,不成功,就會阻塞等待(等待鎖資源釋放)
不同線程對于鎖的競爭能力可能會不同,在純互斥環境中,如果鎖分配不夠合理,容易導致其他線程的饑餓問題----->不是說只要有互斥就必有饑餓,適合純互斥場景就用互斥。
1.3 初談互斥與同步
目前,我們對于鎖的概念已經有了一個清晰的認識了,但是我們發現了一個新的問題,當一個線程申請鎖,完成對臨界資源的訪問后,釋放鎖后,該線程可能也會申請鎖,這就可能出現一個線程一直在申請和釋放鎖,導致其他線程沒辦法申請到鎖,對于這種情況,就要深入了解同步的概念來解決新出現的問題,現在讓我們更加深入的了解互斥與同步吧
現在有一個vip自習室:
vip自習室規定:1. 外面的同學想要進入vip自習室必須排隊。2. 出來的同學,將鑰匙放好后,不能立馬重新拿鑰匙進vip自習室,如果想要再次進入自習室則必須排到隊列的尾部進行排隊
vip自習室的規則讓所有的同學(線程)按照一定的順序拿到鑰匙(鎖)進入vip自習室,而按照一定的順序性獲取資源的模式就是同步!!!
1.4 鎖的原理
鎖本身也是一種臨界資源!!!所以。申請鎖和釋放鎖本身就被設計成為了原子性操作了(問題:如何做到的???)
在臨界區中,線程可以被切換嗎?可以切換!!!在線程被切出去的時候,是持有鎖被切走的。該線程即使被切換走了,照樣沒有任何線程能進入資源臨界區訪問臨界資源!
對于其他線程來講,一個線程要么沒有鎖,要么釋放鎖。當前線程訪問臨界區的過程,對于其他線程就是原子的!!!
問題一:為什么說ticket–不是原子的?因為該語句會變成多條匯編語句,在該匯編語句的中間,如果有其他線程也在執行,就會出錯,出現不一致的情況,換言之,只要匯編語句只有一條就沒有問題,所以,原子:只有一條匯編語句就是原子的!!!
lock的匯編語句:xchqb %al,mutex
Xchqb交換的本質:把內存中的數據(共享),交換到CPU的寄存器中(把數據交換到線程的硬件的上下文中)—>把一個共享的鎖,讓一個線程以一條匯編語句的方式,交換到自己的上下文中!!!(這就叫做當前線程持有鎖了!!!)
unlock的匯編語句:movb $1,mutex:將一個共享的鎖從線程的上下文中拿出來。(這就叫做當前線程釋放鎖了!!!)
1.5 可重入VS線程安全
線程安全的概念:多個線程并發同一段代碼時,不會出現不同的結果。常見對全局變量或者靜態變量進行操作, 并且沒有有鎖保護的情況下,會出現該問題。
重入的概念:同一個函數被不同的執行流調用,當前一個流程還沒有執行完,就有其他的執行流再次進入,我們稱之為重入。一個函數在重入的情況下,運行結果不會出現任何不同或者任何問題,則該函數被稱為可重入函數、不則具不可重入函數。
可重入與線程安全聯系:
函數是可重入的,那就是線程安全的。
函數是不可重入的,那就不能由多個線程使用,有可能引發線程安全問題。
如果一個函數中有全局變量,那么這個函數既不是線程安全也不是可重入的。
可重入與線程安全區別:
可重入函數是線程安全函數的一種。
線程安全不一定是可重入的,而可重入函數則一定是線程安全的。
如果將對臨界資源的訪問加上鎖,則這個函數是線程安全的,但如果這個重入函數若鎖還未釋放則會產生死鎖,因此是不可重入的。
1.6 死鎖
死鎖的概念:
死鎖是指在一組進程中的各個進程均占有不會釋放的資源,但因互相申請被其他進程所站用不會釋放的資源而處于的一種永久等待狀態。
死鎖產生的必要條件:
互斥條件:一個資源每次只能被一個執行流使用(前提)
請求與保持條件:一個執行流因請求資源而阻塞時,對已獲得的資源保持不放(原則)
不剝奪條件:一個執行流已獲得的資源,在末使用完之前,不能強行剝奪(原則)
死鎖產生的充分條件:
循環等待條件:若干執行流之間形成一種頭尾相接的循環等待資源的關系(重要條件)
這三個必要條件必須同時滿足才是死鎖。
如何避免死鎖問題?
1. 破壞死鎖的四個必要條件,只需要一個不滿足就可以了
2. 加鎖順序一致
3. 避免鎖未釋放的場景
4. 資源一次性分配
1.7 避免死鎖的算法(擴展)
銀行家算法:
下面是銀行家算法的模擬實現,感興趣的小伙伴可以去了解
銀行家算法避免死鎖
總結:
本篇博客先是補充了上一章中對于線程分離的知識缺失的內容補全,而后,從一小段代碼出發,在多線程的搶票模型下,我們逐步發現多線程帶來的問題,并逐步解決,為了解決這些問題,我們先后引入了互斥和同步的概念,最后有隊線程安全問題和可重入的問題進行了了解,并講述了死鎖的概念及其產生的條件,最后以避免死鎖的銀行家算法結尾,謝謝大家的支持!!!