目錄
🚩看現象,說原因
🚩解決方案
🚩互斥鎖
?🚀關于互斥鎖的理解
🚀關于原子性的理解
🚀如何理解加鎖和解鎖是原子的
🚩對互斥鎖的簡單封裝
引言
大家有任何疑問,可以在評論區留言或者私信我,我一定盡力解答。
今天我們學習Linux線程互斥的話題。Linux同步和互斥是Linux線程學習的延伸。但這部分挺有難度的,請大家做好準備。那我們就正式開始了。
🚩看現象,說原因
我們先上一段代碼:
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<cassert>
using namespace std;
int NUM=5;
int ticket=1000;
class pthread
{
public:char buffer[1024];pthread_t id;
};
void *get_ticket(void *args)
{pthread *pth=static_cast<pthread*>(args);while(1){usleep(1234);if(ticket<0){return nullptr;}cout<<pth->buffer<<" is ruuing ticket: "<<ticket<<endl;ticket--;}
}int main()
{vector<pthread*> pthpool;for(int i=0;i<NUM;i++){pthread* new_pth=new pthread();snprintf(new_pth->buffer,sizeof (new_pth->buffer),"thread-%d",i+1);int n=pthread_create(&(new_pth->id),nullptr,get_ticket,new_pth);assert(n==0);(void)n;pthpool.push_back(new_pth);}for(int i=0;i<pthpool.size();i++){int m= pthread_join(pthpool[i]->id,nullptr);assert(m==0);(void)m;}return 0;}
這段代碼模擬的是搶票模型,一共有一千張票,我們讓幾個線程同時去搶票。看看有什么不符合實際的情況發生。
還真有不符合實際的情況發生:竟然搶到了負票。臥槽,這是什么情況,我們趕緊分析一下。
首先,在代碼中我們定義了一個全局變量:ticket 。這個變量被所有線程所共享。
對于這種情形,我們直接拉向極端情況:假設此時的票數只有一張了。一個線程進入if內部,但是對票數還沒有進行操作,這時,時間片到了,這個線程被切了下去。緊接著,一個線程就通過if判斷,順利搶到了最后一張票,對票數進行了操作。此時已經無票可搶了。這時,那個被切下來的線程又帶著它的數據開始了搶票。但是在這個線程看來,票數依舊還有最后一張,所以,它又對票數進行了減減操作,得到了負票。
這種情況顯然是不合理的,假如一個電影院有100個座位,結果賣出去102張票,這怎么可以呢?
我們定義的全局變量,在沒有保護的情況下,往往曬不安全的。像上面多個線程在交替執行時造成的數據安全問題,我們稱之為出現了數據不一致問題。
這就是個坑啊,必須解決。
🚩解決方案
在提出解決方案之前,我們先回顧幾個概念。
- 多個執行流進行安全訪問的共享資源,叫做臨界資源
- 我們把多個執行流中,訪問臨界資源的代碼叫做臨界區,臨界區往往是線程代碼很小的一部分。
- 想讓多個線程串行訪問共享資源的方式叫做互斥。
- 對一個資源進行訪問的時候,要么不做,要么做完,這種特性叫做原子性。一個對資源進行的操作,如果只有一挑匯編語句完成,那么就是原子的,反之就不是原則的。這是當前我們對原子性的理解,后面還會發生改變。
?我們提出的解決方案就是加鎖。相信大家第一次聽到鎖。對于什么是鎖,如何加鎖,鎖的原理是什么我們都不清楚,別著急,我們在接下來的內容里會進行詳細的詳解。
我們先使用一下鎖,見見豬跑!!
#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<vector>
#include<cassert>
using namespace std;
int NUM=5;
int ticket=1000;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
class pthread
{
public:char buffer[1024];pthread_t id;
};
void *get_ticket(void *args)
{pthread *pth=static_cast<pthread*>(args);while(1){ pthread_mutex_lock(&mutex);usleep(1234);if(ticket<0){ pthread_mutex_unlock(&mutex);return nullptr;}cout<<pth->buffer<<" is ruuing ticket: "<<ticket<<endl;ticket--;pthread_mutex_unlock(&mutex);}
}int main()
{vector<pthread*> pthpool;for(int i=0;i<NUM;i++){pthread* new_pth=new pthread();snprintf(new_pth->buffer,sizeof (new_pth->buffer),"user-%d",i+1);int n=pthread_create(&(new_pth->id),nullptr,get_ticket,new_pth);assert(n==0);(void)n;pthpool.push_back(new_pth);}for(int i=0;i<pthpool.size();i++){int m= pthread_join(pthpool[i]->id,nullptr);assert(m==0);(void)m;}return 0;}
?
結果顯示搶票的過程非常順利,接下來,我們把重心指向鎖。
🚩互斥鎖
?首先,我們先認識一些鎖的常見接口
// 所有鎖的相關操作函數都在這個頭文件下
//這些函數如果又返回值,操作成功的話,返回0,失敗的話。返回錯誤碼。錯誤原因被設置
#include <pthread.h>
// 鎖的類型,用來創建鎖
pthread_mutex_t
// 對鎖進行初始化,第二個參數一般設位null
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 如果這個鎖沒有用了,可以調用該函數對鎖進行銷毀
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 如果創建的鎖是全局變量,可以這樣初始化。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 對特定代碼部分進行上鎖,這部分代碼只能有一次只能有一個執行流進入,被保護的資源叫做臨界資源。
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 嘗試上鎖,不一定成功。
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 取消鎖。
int pthread_mutex_unlock(pthread_mutex_t *mutex);
剛剛,我們已經使用一種方式實現了加鎖,接下來,我們用另一種方式:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cassert>
using namespace std;
int NUM = 5;
int ticket = 1000;
class Thread_Data
{
public:Thread_Data(string name,pthread_mutex_t* mutex):_name(name),_mutex(mutex){}~Thread_Data(){}
public:string _name;pthread_mutex_t* _mutex;
};
void *get_ticket(void *args)
{Thread_Data *pth = static_cast<Thread_Data*>(args);while (1){pthread_mutex_lock(pth->_mutex); if (ticket > 0){ usleep(1234); cout << pth->_name << " is ruuing ticket: " << ticket << endl; ticket--;pthread_mutex_unlock(pth->_mutex); } else{pthread_mutex_unlock(pth->_mutex);break;}}
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex,nullptr);vector<pthread_t> tids(NUM); for (int i = 0; i < NUM; i++){char buffer[1024];Thread_Data *td=new Thread_Data(buffer,&mutex);snprintf(buffer, sizeof(buffer), "user-%d", i + 1);int n =pthread_create(&tids[i], nullptr, get_ticket, td);assert(n == 0);(void)n;}for (int i = 0; i < tids.size(); i++){int m = pthread_join(tids[i], nullptr);assert(m == 0);(void)m;}return 0;
}
?
運行一下,發現一直是4號線程在跑,其他線程呢?我也沒讓其他線程退出呀!而且搶票的時間變長了。
- 加鎖和解鎖是多個線程串行進行的,所以程序允許起來會變得很慢。
- 鎖只規定互斥訪問,沒有規定誰優先訪問。
- 鎖就是讓多個線程公平競爭的結果,強者勝出嘛。
?🚀關于互斥鎖的理解
- 所有的執行流都可以訪問這一把鎖,所以鎖是一個共享資源。
- 加鎖和解鎖的過程必須是原子的,不會存在中間狀態。要么成功,要么失敗。加鎖的過程必須是安全的。
- 誰持有鎖,誰進入臨界區。
?如果一個執行流申請鎖成功,繼續向后運行;如果申請失敗的話,這個執行流怎么辦?
這種情況試一試不就知道了。我們依舊使用上面的一份代碼,稍稍做一下修改:
?
所以,當一個執行流申請鎖失敗時,這個執行流會阻塞在這里。
🚀關于原子性的理解
如圖,三個執行流
?
問:如果線程1申請鎖成功,進入臨界資源,正在訪問臨界資源區的時候,其他線程在做什么?
?答:都在阻塞等待,直到持有鎖的線程釋放鎖。
問; 如果線程1申請鎖成功,進入臨界資源,正在訪問臨界資源區的時候,可不可以被切換?
答:絕對是可以的,CPU管你有沒有鎖呢,時間片到了你必須下來。當持有鎖的線程被切下來的時候,
是抱著鎖走的,即使自己被切走了,其他線程依舊無法申請鎖成功,也就無法繼續向后執行。
這就叫作:江湖上沒有我,但依舊有我的傳說。
所以對于其他線程而言,有意義的鎖的狀態,無非兩種:①申請鎖前,②釋放鎖后
?所以,站在其他線程的角度來看待當前持有鎖的過程,就是原子的。
?所以,未來我們在使用鎖的時候,要遵守什么樣的原則呢?
- 一定要保證代碼的粒度(鎖要保護的代碼的多少i)要非常小。
- 加鎖是程序員的行為,必須做到要加的話所有的線程必須要加鎖。
🚀如何理解加鎖和解鎖是原子的
?在分析如何實現加鎖和解鎖之前,我們先形成幾個共識:
- CPU內執行流只有一套,且被所有執行流所共享。
- CPU內寄存器的內容屬線程所有,是每個執行流的上下文。時間片到達,數據帶走。
- 在進行加鎖和解鎖的時候,這個線程隨時會因時間片已到而被換下來。
為了實現互斥鎖操作,大多數體系結構都提供了swap或exchange指令,該指令的作用是把寄存器和內存單元的數據相交換,由于只有一條指令,保證了原子性?。
如圖:
我們假設有線程A,B兩個線程,A想要獲得鎖?
鎖內存儲的數據就是int類型的1。?A線程中有數字0。
①:movb $0,%al:將線程A中的1move到寄存器中。此時,是有可能發生時間片到達的,但是寄存器內的數據屬于線程A,線程A是要帶走的。
②:xchgb %al,mutex:將鎖中的數據和寄存器內的數據進行交換。此時寄存器內的數據變成1,鎖中的數據變為0。這是關鍵的一步,也有可能會發生切換。假設不巧的很,A線程被切下去了,B線程被切上來了。B線程從第一步開始,走到現在,寄存器內的數據應該是0。然后進入判斷體eles進行掛起等待。
③如果在第二步中線程A被切下來,等待一段時間,時間片再次輪到線程A時,A將自己的數據加載到寄存器內進入判斷,然后獲得鎖。
交換的過程由一條匯編構成
交換的本質:共享的數據,交換到線程的上下文中。?
那么。如何完成解鎖的操作呢。解鎖的操作特別簡單,只需一步。
將寄存器內的1歸還給鎖。然后return返回就可以了。
🚩對互斥鎖的簡單封裝
相信大家對互斥鎖都有了充分的了解。接下來,我們就實現一下對互斥鎖的簡單封裝。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cassert>class Mutex
{
public:Mutex(pthread_mutex_t *mutex) : _mutex(mutex){}void unlock(){if (_mutex){pthread_mutex_unlock(_mutex);}}void lock(){if(_mutex){pthread_mutex_lock(_mutex);}}~Mutex(){}public:pthread_mutex_t *_mutex;
};
class Lockguard
{
public:Lockguard(Mutex mutex) : _mutex(mutex){_mutex.lock();}~Lockguard(){_mutex.unlock();}public:Mutex _mutex;
};
這種利用變量出了函數作用域自動銷毀的性質,我們稱之為RAII特性。
到這里,我們本篇的內容也就結束了,我們期待下一期博客相遇。
?
?