??作者:一只大喵咪1201
??專欄:《Linux學習》
??格言:你只管努力,剩下的交給時間!
一、 信號量
之前在學習進程間通信的時候,本喵簡單的介紹過一下信號量,今天在這里進行詳細的介紹。
這是之前寫的基于阻塞隊列的生產(chǎn)者消費者模型中向阻塞隊列中push任務的代碼。
- 上面代碼中有什么不足之處嗎?
一個線程在向阻塞隊列中push任務的時候,必須滿足臨界資源不滿的條件,否則就會被放入到條件變量的等待隊列中去。
但是臨界資源是否為滿是不能直接得到答案的,需要先申請鎖,然后進入臨界區(qū)訪問臨界資源去判斷它是否為滿。
- 在判斷臨界資源是否滿足條件的過程中,必須先加鎖,再檢測,再操作,最后再解鎖。
檢測臨界資源的本質(zhì)也是在訪問臨界資源。
只要對臨界資源整體加鎖,就默認現(xiàn)場會對這個臨界資源整體使用,但是實際情況可能存在:一份臨界資源,劃分為多個不同的區(qū)域,而且運行多個線程同時訪問不同的區(qū)域。
之前代碼的不足之處:
- 在訪問臨界資源之前,無法得知臨界資源的情況。
- 多個線程不能同時訪問臨界資源的不同區(qū)域。
1.1 概念
為此,提出了信號量來解決之前代碼的不足。
- 信號量:本質(zhì)是一把計數(shù)器,用來衡量臨界資源中資源數(shù)量多少。
- 申請信號量的本質(zhì):對臨界資源中特定的小塊資源的預定機制。
信號量也是一種互斥量,只要申請到信號量的線程,在未來一定能夠擁有一份臨界資源。
如上圖所示,將一塊臨界資源劃分為9個不同的區(qū)域,現(xiàn)在想要讓多個線程同時訪問這9個不同的區(qū)域。
- 創(chuàng)建一個信號量,它的值是9。
- 每一個來訪問臨界資源的線程都先申請信號量,也就是將計數(shù)值減一。
- 當計數(shù)值被減到0的時候,說明臨界資源中的9個區(qū)域都有現(xiàn)場再訪問,其他想要訪問臨界資源的現(xiàn)場只能阻塞等待。
申請到信號量的現(xiàn)場就可以進入臨界區(qū)去訪問臨界資源,當訪問完畢以后,再將信號量加一。
- 每個線程訪問臨界資源中的哪塊區(qū)域由程序員決定,但是必須保證一個區(qū)域只能有一個現(xiàn)場在訪問。
通過信號量的方式就解決了之前代碼的不足:
- 線程不用訪問臨界資源就可以知道資源的使用情況。
信號量只要申請成功就一定有資源使用,只要申請失敗就說明條件不滿足,只能阻塞等待。
- 臨界資源中的不同區(qū)域可以被多線程同時訪問。
1.2 信號量的基本操作
所有線程必須都能看到信號量才能申請,所以信號量是一個公共資源,公共資源就涉及到線程安全問題。
根據(jù)上面分析,信號量的基本操作就是對信號量進行加一和減一,所以這兩個操作是原子的。
- P操作:就是信號量減減(sem–),也就是在申請資源,而且該操作必須是原子的。
- V操作:就是信號量加加(sem++),也就是在歸還資源,同樣也必須是原子的。
1.3 信號量的基本使用接口
#include <semaphore.h>//信號量必須包含的頭文件
sem_t sem;//創(chuàng)建信號量
初始化信號量:
int sem_init(sem_t* sem, int pshared, unsigned int value);
- sem:信號量指針
- shared:0表示線程間共享,非0表示進程間共享。我們一般情況下寫0就行。
- value:信號量初始值,也就是計數(shù)器的值。
- 返回值:成功返回0,失敗返回-1,并且設置errno。
信號量銷毀:
int sem_destroy(sem_t* sem);
- sem:信號量指針
- 返回值:成功返回0,失敗返回-1,并且設置errno。
等待信號量:
int sem_wait(sem_t* sem);//P操作
- sem:信號量指針。
- 返回值:成功返回0,失敗返回-1,并且設置errno。
- 功能:等待信號量,會將信號量的值減1,也就是在申請信號量。
發(fā)布信號量:
int sem_post(sem_t* sem);//V操作
- sem:信號量指針。
- 返回值:成功返回0,失敗返回-1,并且設置errno。
- 功能:發(fā)布信號量,表示資源使用完畢,可以歸還資源,將信號量值加1,也就是在歸還信號量。
這些接口和前面mutex的接口非常類似,因為他們都是POSIX標準的,所以使用起來沒有任何難度。
二、基于環(huán)形隊列的生產(chǎn)者消費者模型
這里使用信號量來實現(xiàn)一個單生產(chǎn)單消費的環(huán)形隊列模型。
- 環(huán)形隊列采用數(shù)組來模擬,用取模運算來模擬環(huán)狀特性。
- 環(huán)形結構起始狀態(tài)和結束狀態(tài)都是一樣的,不好判斷為空或者為滿。
- 當環(huán)形隊列為空時,頭和尾都指向同一個位置。
- 當環(huán)形隊列為滿時,頭和尾也都指向同一個位置。
- 可以通過加計數(shù)器或者標記位來判滿或者空,也可以預留一個空的位置,作為滿的狀態(tài)。
- 但是我們現(xiàn)在有信號量這個計數(shù)器,就不需要用數(shù)據(jù)結構的方式來判空和判滿了,能夠很簡單的進行多線程間的同步。
2.1 分析
單生產(chǎn)者和單消費者一共兩個線程在訪問環(huán)形隊列這個公共資源,生產(chǎn)者向環(huán)形隊列中生產(chǎn)數(shù)據(jù),消費者從環(huán)形隊列中消費數(shù)據(jù)。
- 生產(chǎn)者和消費者什么情況下會訪問同一個位置?
- 環(huán)形隊列為空的時候,生產(chǎn)者和消費者會訪問同一個位置。
當隊列為空的時候,生產(chǎn)者訪問隊尾,向隊列中生產(chǎn)數(shù)據(jù),消費者訪問對首消費數(shù)據(jù),由于環(huán)形隊列且為空,所以隊首和隊尾是同一個位置。
- 環(huán)形隊列為滿的時候,生產(chǎn)者和消費者會訪問同一個位置。
當環(huán)形隊列只有一個空位置的時候,生產(chǎn)者訪問隊尾生產(chǎn)數(shù)據(jù),生產(chǎn)完畢后指向下一個位置,由于環(huán)形隊列且為滿,所以此時生產(chǎn)者又指向了隊首,和消費者訪問同一個位置。
- 其他任何時候,生產(chǎn)者和消費者訪問的都是不同的區(qū)域。
只要環(huán)形隊列不滿也不空,那么生產(chǎn)者和消費者之間都有數(shù)據(jù),它們各自訪問各自的區(qū)域。
- 為了完成環(huán)形隊列的生產(chǎn)消費問題,必須要做的核心工作是什么?
- 消費者不能超過生產(chǎn)者。
消費者消費的是生產(chǎn)者生產(chǎn)的數(shù)據(jù),生產(chǎn)者沒有生產(chǎn),消費者就無法消費。當消費者超過生產(chǎn)者后,消費者訪問的區(qū)域并沒有數(shù)據(jù),所以沒有任何意義。
消費者必須跟著生產(chǎn)者的后面,即使消費速度非常快(導致環(huán)形隊列為空),此時消費者和生產(chǎn)者訪問同一區(qū)域。
- 生產(chǎn)者不能把消費者套一圈以上
消費者消費的速度比較慢,環(huán)形隊列滿了以后,如果生產(chǎn)者繼續(xù)生產(chǎn),就會將消費者還沒來得及消費的數(shù)據(jù)覆蓋,消費者就無法消費到覆蓋之前的數(shù)據(jù)了。
- 對于生產(chǎn)者而言,它在意的是環(huán)形隊列中空閑的空間。
生產(chǎn)者只負責將數(shù)據(jù)生產(chǎn)到環(huán)形隊列的空間中,當環(huán)形隊列滿了以后就不能生產(chǎn)了,所以它只關心環(huán)形隊列中有多少空間可以用來生成數(shù)據(jù)。
- 對于消費者而言,它在意的是環(huán)形隊列中數(shù)據(jù)的個數(shù)。
消費者只負責從環(huán)形隊列中消費數(shù)據(jù),當環(huán)形隊列為空時就停止消費,所以它只關心環(huán)形隊列中有多是個數(shù)據(jù)可以用來消費。
- 空間資源定義一個信號量。用來統(tǒng)計空閑空間的個數(shù)。
- 數(shù)據(jù)資源定義一個信號量。用來統(tǒng)計數(shù)據(jù)個數(shù)。
所以生產(chǎn)者每次在訪問臨界資源之前,需要先申請空間資源的信號量,申請成功就可以進行生產(chǎn),否則就阻塞等待。
消費者在訪問臨界資源之前,需要申請數(shù)據(jù)資源的信號量,申請成功就可以消費數(shù)據(jù),否則就阻塞等待。
- 空間資源信號量的申請由生產(chǎn)者進行,歸還(V操作)由消費者進行,表示生產(chǎn)者可以生產(chǎn)數(shù)據(jù)。
- 數(shù)據(jù)資源信號量的申請有消費者進行,歸還(V操作)由生產(chǎn)者進行,表示消費者可以進行消費。
在信號量的初始化時,空間資源的信號量為環(huán)形隊列的大小,因為沒有生產(chǎn)任何數(shù)據(jù)。數(shù)據(jù)資源的信號量為0,因為沒有任何數(shù)據(jù)可以消費。
通過信號量的方式同樣維護了環(huán)形隊列的核心操作,消費者消費速度快時,會將數(shù)據(jù)資源信號量全部申請完,但是此時生產(chǎn)者沒有生產(chǎn)數(shù)據(jù),也就沒有歸還數(shù)據(jù)資源的信號量,所以消費者會阻塞等待,不會超生產(chǎn)者。
生產(chǎn)者生產(chǎn)速度快時,會將空間資源信號量全部申請完,但是此時消費者沒有消費數(shù)據(jù),也就沒有歸還空間資源的信號量,所以生產(chǎn)者會阻塞等待,不會超過套消費者一個圈。
生產(chǎn)者偽代碼:
productor_sem = 環(huán)形隊列大小;
P(productor_sem);//申請空間資源信號量
//申請成功,繼續(xù)向下運行。
//生氣失敗,阻塞在申請?zhí)帯?/span>
.......//從事生產(chǎn)活動——把數(shù)據(jù)放入隊列中
V(comsumer_sem);//歸還數(shù)據(jù)資源信號量
消費者偽代碼:
comsumer_sem = 0;
P(comsumer_sem);//申請數(shù)據(jù)資源信號量
//申請成功,繼續(xù)向下運行。
//生氣失敗,阻塞在申請?zhí)帯?/span>
.......//從事消費活動——從隊列中消費數(shù)據(jù)
V(proudctor_sem);//歸還空間資源信號量
在環(huán)形隊列中,大部分情況下單生產(chǎn)和單消費是可以并發(fā)執(zhí)行的,只有在滿或者空的時候,才會有同步和互斥問題,同步和互斥是通過信號量來實現(xiàn)的。
- 在生產(chǎn)者和消費者并發(fā)訪問環(huán)形隊列時,訪問的位置其實就是隊列的下標,而且是兩個下標。
- 當空或者滿的時候,兩個下標相同。
2.2 代碼實現(xiàn)
RingQueue.hpp:
const int RingQueueSize = 5;//環(huán)形隊列默認大小
template<class T>
class RingQueue
{
private:
//申請信號量
void P(sem_t& sem)
{
int n = sem_wait(&sem);
assert(n == 0);
(void)n;
}
//歸還信號量
void V(sem_t& sem)
{
int n = sem_post(&sem);
assert(n == 0);
(void)n;
}
public:
//構造函數(shù)
RingQueue(int capacity = RingQueueSize)
:_capacity(capacity)
,_queue(capacity)//初始化環(huán)形隊列大小
{
//初始化空間資源信號量,計數(shù)值為環(huán)形隊列大小
int n = sem_init(&_spaceSem,0,_capacity);
assert(n == 0);
//初始化數(shù)據(jù)資源信號量,計數(shù)值為0
n = sem_init(&_dataSem,0,0);
assert(n == 0);
(void)n;
//初始化生產(chǎn)者和消費者訪問下標
_productorIndex = _comsumerIndex = 0;
}
//向隊列中生產(chǎn)數(shù)據(jù)
void Push(const T& in)
{
P(_spaceSem);//先申請空間資源信號量
_queue[_productorIndex++] = in;//生產(chǎn)數(shù)據(jù)
_productorIndex%=_capacity;//維護環(huán)形
V(_dataSem);//歸還數(shù)據(jù)資源信號量
}
//從隊列中消費數(shù)據(jù)
void Pop(T* out)
{
P(_dataSem);//先申請數(shù)據(jù)資源信號量
*out = _queue[_comsumerIndex++];//消費數(shù)據(jù)
_comsumerIndex%=_capacity;//維護環(huán)形隊列
V(_spaceSem);//歸還空間資源信號量
}
//析構函數(shù)
~RingQueue()
{
//銷毀空間資源和數(shù)據(jù)資源的信號量
int n = sem_destroy(&_spaceSem);
assert(n == 0);
n = sem_destroy(&_dataSem);
assert(n == 0);
(void)n;
}
private:
std::vector<T> _queue;//模擬環(huán)形隊列
int _capacity;//統(tǒng)計數(shù)據(jù)個數(shù)
sem_t _spaceSem;//空間信號量
sem_t _dataSem;//數(shù)據(jù)信號量
int _productorIndex;//生產(chǎn)者訪問下標
int _comsumerIndex;//消費者訪問下標
};
RingQueue類模板中包括隊列,容量,空間資源和數(shù)據(jù)資源的信號量,生產(chǎn)者和消費者訪問隊列的下標,根據(jù)前面的分析一步步寫出即可。
main.cpp:
//獲取當前線程名字tid
std::string Self_Name()
{
char nameBuffer[64];
snprintf(nameBuffer,sizeof nameBuffer,"tid:0x%x",pthread_self());
return nameBuffer;
}
//生產(chǎn)者線程
void* Productor_Routine(void* args)
{
RingQueue<CalTask>* rq = static_cast<RingQueue<CalTask>*>(args);
while(1)
{
//構建任務
int x = rand()%10;
int y = rand()%10;
char op = oper[rand()%oper.size()];
CalTask t(x,y,op,myath);
//生產(chǎn)任務
rq->Push(t);
std::cout<<Self_Name()<<",生產(chǎn)任務:"<<t.toTaskString()<<std::endl;
}
}
//消費者線程
void* Comsumer_Routine(void* args)
{
RingQueue<CalTask>* rq = static_cast<RingQueue<CalTask>*>(args);
while(1)
{
sleep(1);
CalTask t;
rq->Pop(&t);//消費任務
std::string result = t();//處理任務
std::cout<<Self_Name()<<",消費任務:"<<result<<std::endl;
}
}
生產(chǎn)者線程生產(chǎn)計算任務,并且生產(chǎn)到環(huán)形隊列中,消費者線程從環(huán)形隊列中消費認為,并且執(zhí)行對應的計算任務。
- 只有rq->Push(t)和rq->Pop(&t)兩句是在訪問環(huán)形隊列,其他都是非臨界區(qū)。
- 這里生產(chǎn)消費的任務使用的是之前學習阻塞隊列的生產(chǎn)者消費者模型中的計算任務。
- 可以向環(huán)形隊列中生產(chǎn)任何任務,消費者線程都不用知道認為是什么。
貼圖計算任務代碼:
運行結果:
創(chuàng)建消費者線程和生產(chǎn)者線程后,運行結構如上圖右邊所示。
- 由于生產(chǎn)者線程是沒有延時,所以在線程一啟動后就將環(huán)形隊列全部生產(chǎn)滿。
- 消費者線程在延時一秒后開始從環(huán)形隊列中消費任務,執(zhí)行相應的計算。
- 消費者每消費一個任務環(huán)形隊列中就有一個位置,生產(chǎn)者接著生產(chǎn)一個任務,而且消費是按照生產(chǎn)的順序進行的。
同樣可以讓生產(chǎn)者延時一秒,消費者不延時,有興趣的小伙伴自行觀察現(xiàn)象。
2.3 多生產(chǎn)多消費
環(huán)形隊列的生產(chǎn)者消費者模型同樣遵循321原則:
- 3:三種關系,生產(chǎn)者和生產(chǎn)者(互斥關系),消費者和消費者(互斥關系),生產(chǎn)者和消費者(在隊列為空或者滿時——同步和互斥關系)。
- 2:兩種角色,生產(chǎn)者和消費者。
- 1:一個交易場所,環(huán)形隊列。
上面單消費單生成模型,維護的只是生產(chǎn)者和消費者之間的關系,要想實現(xiàn)多生產(chǎn)多消費,只需要將另外兩種關系維護好即可。
- 在RingQueue中增加兩把互斥鎖,一把生產(chǎn)者使用,一把消費者使用。
- 在構造函數(shù)中將鎖初始化,在析構函數(shù)中將鎖摧毀。
這里本喵就不貼代碼了。
Push是向環(huán)形隊列中生產(chǎn)任務,是生產(chǎn)者在調(diào)用,所以在生產(chǎn)之前需要加鎖。Pop是從環(huán)形隊列中消費認為,是消費者在調(diào)用,所以在消費之前加鎖。
- 互斥鎖和申請信號量誰在前比較合適呢?
如果互斥鎖在前,申請信號量在后:
- 所有生產(chǎn)者線程或者是消費者線程都需要先競爭鎖,然后再去申請信號量,信號量申請成功才能進入臨界區(qū)。
- 如果信號量申請失敗就抱著鎖阻塞,其他同類型線程就無法申請到鎖。
這就好比去電影院買票,必須先排隊進入放映廳才能買票。
如果申請信號量在前,互斥鎖在后:
- 所有生產(chǎn)者線程或者消費者線程先申請信號量,再去申請鎖,然后進入臨界區(qū)。
- 如果信號量申請失敗就不會再去申請鎖。
同樣是電影院,這就好比先買票,然后再排隊進入放映廳,沒買上票就沒必要排隊了。
對于線程來說,申請鎖也是有代價的,將信號量申請放在前面可以減少申請鎖的次數(shù),所以申請信號量在互斥鎖之前更合適。
創(chuàng)建多個生產(chǎn)者線程和多個消費者線程,去執(zhí)行生產(chǎn)計算任務和消費計算任務。
- 生產(chǎn)任務的線程是不同的,可以根據(jù)tid值區(qū)別出來。
- 消費認為的現(xiàn)場也是不同的,同樣可以根據(jù)tid值區(qū)別。
此時就實現(xiàn)了基于環(huán)形隊列的多生產(chǎn)多消費模型。
三、自旋鎖
掛起等待鎖:
之前使用的互斥鎖就是掛起等待鎖。多線程在競爭互斥鎖時,申請到鎖的線程進入臨界區(qū),而沒有申請到鎖的線程阻塞等待。
所謂的阻塞等待,其實是將該現(xiàn)場放入到操作系統(tǒng)維護的等待隊列中,在合適的時候,操作系統(tǒng)再將其喚醒,放到運行隊列中繼續(xù)去申請鎖。
自旋鎖:
自旋鎖也互斥鎖,它的作用也是保護共享資源的安全。多線程在競爭自旋鎖時,申請到鎖的線程進入臨界區(qū),而沒有申請到鎖的線程不會掛起等待。
沒有申請到鎖的線程會不停的繼續(xù)去申請鎖,直到申請鎖成功進入臨界資源,自旋和進程等待中的輪詢非常的相似。
自旋鎖和掛起等待鎖的區(qū)別就在于:沒有申請到鎖時,自旋鎖仍然繼續(xù)申請,掛起等待鎖則進入等待隊列等待,在被喚醒后繼續(xù)申請鎖。
- 掛起等待鎖在的線程掛起后,CPU就暫時不用去調(diào)度它,可以去執(zhí)行其他任務。
- 自旋鎖的線程則會始終占用CPU資源。
是什么決定著線程的等待方式呢?是需要等待的時長。
- 當訪問臨界資源的時間較短的時候,可以使用自旋鎖,因為進入臨界區(qū)的線程會非??斓某鰜?,處于自旋狀態(tài)的線程也可以很快進入臨界區(qū)。
此時申請自旋鎖的線程,免去了被掛起等待和喚醒的過程,一定程度上提高了效率。
- 當訪問臨界資源的時間較長的時候,就要使用掛起等待鎖,因為進入臨界區(qū)的現(xiàn)場不會很快出來。
此時將申請鎖失敗的線程掛起,就將CPU資源空閑了出來,如果不掛起而處于自旋狀態(tài),則CPU就一直被占用。
那么需要等待的時間長短是如何定義的呢?
- 像前面寫的多線程搶票的代碼,對于tickets的訪問就可以使用自旋鎖。
- 對于需要進行復雜運算,高IO,以及等待某些軟件標志就位的情況就是用掛起等待所。
等待時間的長短并沒有明確的定義,使用自旋鎖還是掛起等待鎖根據(jù)具體情況來覺得。最好的方式是分別測試兩種鎖,哪種效率高就用哪種。
3.1 基本使用接口
#include <pthread.h>//使用自旋鎖要包含的頭文件
pthread_spinlock_t lock;//創(chuàng)建自旋鎖
初始化自旋鎖:
int pthread_spin_init(pthread_spinlock_t* lock, int shared);
- lock:自旋鎖指針
- shared:0表示線程間共享,非0表示進程間共享,和信號量初始化中的shared一樣。
- 返回值:成功返回0,失敗返回-1。
銷毀自旋鎖:
int pthread_spin_destroy(pthread_spinlock_t* lock);
- lock:自旋鎖指針
- 返回值:成功返回0,失敗返回-1。
加鎖:
int pthread_spin_lock(pthread_spinlock_t* lock);
- lock:自旋鎖指針
- 返回值:加鎖成功返回0,失敗返回-1。
解鎖:
int pthread_spin_unlock(pthread_spinlock_t* lock);
- lock:自旋鎖指針
- 返回值:成功返回0,失敗返回-1。
這些接口和之前學習的互斥鎖以及信號量非常相似,只是換個函數(shù)名而已,因為它們遵循POSIX標準。
四、讀寫鎖
讀寫鎖主要使用在讀者寫者模型中,讀者寫者模型和生產(chǎn)者消費者模型很類似,也是遵循321原則:
- 3:三種關系,寫者和寫者(互斥),讀者和寫者(同步和互斥),讀者和讀者(沒有關系)。
- 2:兩種角色,讀者和寫者。
- 1:一個交易場所,任意類型的數(shù)據(jù)結構。
讀者線程和寫者線程并發(fā)訪問一塊臨界資源:
- 寫者向臨界資源中寫數(shù)據(jù)。
- 讀者從臨界資源中讀數(shù)據(jù)。
- 讀者和寫者之間是互斥關系
寫者在寫數(shù)據(jù)時,讀者無法訪問臨界資源,因為如果在讀取的時候,寫者還沒有寫完,那么讀者讀到的數(shù)據(jù)就不全。
- 讀者和寫者之間也是同步關系
如果寫者寫好數(shù)據(jù),讀者不去都,那么寫者寫的數(shù)據(jù)就沒有意義,所以寫者寫好數(shù)據(jù)后必須有讀者來讀。
反之,如果所有讀者都已經(jīng)讀取過臨界區(qū)的數(shù)據(jù)了,再讀就是重復的舊數(shù)據(jù),此時讀取也沒有意義,所以讀者讀完數(shù)據(jù)以后,寫者必須來寫入新的數(shù)據(jù)。
- 寫者和寫者直接是互斥關系
如果一個寫者正在寫數(shù)據(jù),另一個寫者也來寫,假設他們寫的是同一塊公共資源,就有可能發(fā)生覆蓋。
- 讀者和讀者之間沒有關系
讀者只從臨界區(qū)中讀取數(shù)據(jù),并不拿走,所以讀者之間并不會產(chǎn)生影響。
讀者寫者模型使用場景:一次發(fā)布,很長時間不做修改,大部分時間都是在被讀取,比如本喵寫的博客。
- 讀者寫者模型 VS 生產(chǎn)者消費者模型的本質(zhì)區(qū)別是:消費者會拿走臨界資源中的數(shù)據(jù),而讀者不會。
有些共享資源的數(shù)據(jù)修改的機會比較少,相比較改寫,它們讀的機會反而高的多。
在讀的過程中,往往伴隨著查找的操作,中間耗時很長。給這種代碼段加鎖,會極大地降低我們程序的效率。
讀寫鎖就是專門用于讀者寫者模型中的一種鎖,可以給讀者加鎖,也可以給寫者加鎖,可以維護讀者寫者的321原則。
臨界區(qū)的狀態(tài) | 讀者請求 | 寫者請求 |
---|---|---|
無鎖 | 可以 | 可以 |
讀鎖 | 可以 | 阻塞 |
寫鎖 | 阻塞 | 阻塞 |
- 持有寫鎖的線程獨占臨界資源,持有讀鎖的線程,讀者之間共享臨界資源。
4.1 基本接口
#include <pthread.h>//讀寫鎖必須包含的頭文件
pthread_rwlock_t rwlock;//創(chuàng)建讀寫鎖
初始化讀寫鎖:
int pthread_rwlock_init(pthread_rwlock_t* rwlock, const pthread_rwlockattrt_t* attr);
- rwlock:讀寫鎖指針
- attr:讀寫鎖屬性結構體指針,一般設置成nullptr即可。
- 返回值:成功返回0,失敗返回-1
銷毀讀寫鎖:
int pthread_rwlock_destroy(pthread_rwlock_t* rwlock);
加讀鎖:
int pthread_rwlock_rdlock(pthread_rwlock_t* rwlock);
加寫鎖:
int pthread_rwlock_wrlock(pthread_rwlock_t* rwlock);
解鎖:
int pthread_rwlock_unlock(pthread_rwlock_t* rwlock);
- 讀鎖和寫鎖都通過這個接口去解鎖。
同樣是POSIX標準,所以返回值,參數(shù)等風格和前面的互斥鎖,信號量,以及自旋鎖一樣,本喵就不詳細解釋了。
4.2 加鎖原理
- 讀寫鎖:在任何時刻,只允許一個寫者寫入,但是允許多個讀者并發(fā)讀取(寫者阻塞)。
是不是感覺非常奇怪?上面的接口中明明只有一把鎖,但是可以給讀者和寫者分別加鎖,而且對于讀者和寫者的效果還不同?
下面本喵寫一段偽代碼來解釋一下:
pthread_rwlock_t lock;
讀寫鎖的類型是一個結構體,它里面封裝的也是互斥鎖,而且針對讀者有一把,針對寫者有一把,只是機制不一樣而已
讀加鎖偽代碼: pthread_rwlock_rdlock(pthread_rwlock_t* rwlock)
pthread_mutex_t rdlock;//創(chuàng)建讀鎖
int reader_count = 0;//讀者計數(shù)
------------------------------------------------------------
lock(&rdlock);//讀加鎖
reader_count++;//讀者數(shù)量加一
if(reader_count == 1)
{
//只要有讀者在訪問臨界資源,就將寫鎖也申請走
lock(&rwlock);//寫加鎖
}
unlock(&rdlock);//解讀鎖
------------------------------------------------------------
//讀取數(shù)據(jù)....
------------------------------------------------------------
lock(&rdlock);//再次讀加鎖
read_count--;//讀者數(shù)量減一
if(reader_count == 0)
{
//讀者全部讀完以后,釋放寫鎖
unlock(&rwlock);//寫解鎖
}
unlock(&rdlock);//讀解鎖
加讀鎖時,有一個計數(shù)器,該計數(shù)器所有讀者線程共享,是一份共享資源,用來統(tǒng)計訪問公共資源的讀者數(shù)量。
偽代碼解釋:
- 每個讀者訪問公共資源的時候,都需要將計數(shù)值加1,考慮到線程安全,所以計數(shù)值要加鎖。
- 當?shù)谝粋€讀者到來后,它先申請了讀鎖,然后又申請了寫鎖,此時寫者線程就無法訪問臨界資源了,因為寫鎖在讀者手里。之后的讀者線程僅將計數(shù)值加一即可。
- 當讀者線程訪問完計數(shù)值以后就將讀鎖解鎖,然后去公共資源中讀數(shù)據(jù)(僅讀取,不拿走)。
- 讀者讀完數(shù)據(jù)以后,繼續(xù)線程安全的訪問計數(shù)值,將值減一,當值被減到0時,說明沒有讀者再來讀數(shù)據(jù)了,此時將申請的寫鎖解鎖,好方便寫者訪問公共資源。
通過這樣的方式就實現(xiàn)了讀者和寫者之前的互斥,讀者和讀者之間沒有關系。
- 互斥訪問讀者計數(shù)值非常的快,讀者真正訪問公共資源的時候是沒有任何關系的(不存在加鎖)。
寫加鎖偽代碼: pthread_rwlock_wrlock(pthread_rwlock_t* lock)
pthread_mutex_t wrlock;//創(chuàng)建寫鎖
------------------------------------------------------------
lock(&wrlock);//寫加鎖
//向臨界資源中寫入數(shù)據(jù)
unlock(&wrlock);//寫解鎖
寫者的加鎖解鎖,實現(xiàn)了寫者之間的互斥關系。
解釋:
- 寫者線程在訪問臨界資源的時候會先申請鎖,申請成功的進入臨界區(qū),失敗的阻塞等待。
- 如果寫者申請寫鎖成功,那么第一個讀者在申請寫鎖的時候同樣會阻塞,直到寫者釋放鎖。
- 如果第一個讀者申請寫鎖成功,那么寫者在申請寫鎖的時候也會阻塞,直到讀者釋放鎖。
寫鎖的原理非常簡單,正是由于讀者會申請寫鎖,寫者也會申請寫鎖,所以才能實現(xiàn)寫者和讀者的互斥。
4.3 讀寫優(yōu)先級
上面講解的讀寫鎖是讀者優(yōu)先的,前提是有讀者已經(jīng)在訪問公共資源。
- 已經(jīng)有讀者在訪問公共資源的時候,寫鎖已經(jīng)被讀者申請走了。
- 當后面寫者和讀者同時到來的時候,寫者會因為無法申請鎖而阻塞,而讀者可以訪問公共資源。
如果沒有讀者在訪問公共資源,第一個讀者和寫者同時到來時,它兩就不存在優(yōu)先關系,誰的競爭能力強誰申請到寫鎖,進入臨界資源。
試想,讀者非常多,那么寫者就始終無法進入臨界區(qū)訪問臨界資源,所以就會導致寫者饑餓問題,但是讀寫鎖就是應用在這種場景下,寫者數(shù)量少執(zhí)行少,讀者數(shù)量多執(zhí)行多。
- 讀寫鎖是可以設置成寫者優(yōu)先的。
- 即使已經(jīng)有讀者在訪問公共資源,并且寫鎖已經(jīng)被申請走了。
- 當后面的寫者和讀者同時到來的時候,將寫者后面的所有讀者阻塞,不讓它們訪問公共資源。
- 當進入臨界區(qū)的讀者出來以后,并且歸還了寫鎖,此時寫者直接申請寫鎖并進入臨界區(qū)訪問臨界資源。
大概的道理是這樣,具體如何阻塞寫者之后的讀者策略可以在代碼層面進行設計。
pthread庫其實是提供了設置讀寫優(yōu)先級的接口的:
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t* attr, int pref);
- attr:屬性設置
- pref:有三種選擇
- PTHREAD_RWLOCK_PREFER_READER_NP:(默認設置)讀者優(yōu)先,可能會導致寫者饑餓情況。
- PTHREAD_RWLOCK_PREFER_WRITER_NP:寫者優(yōu)先
- PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:寫者優(yōu)先,但寫者不能遞歸加鎖。
五、總結
至此本喵已經(jīng)把常用的鎖介紹完了,鎖的種類非常的多,但是常用的就這幾種,尤其是是mutex(掛起等待鎖),cond(條件變量),sem(信號量),以及基于阻塞隊列和環(huán)形隊列的生產(chǎn)者消費者模型尤為重要。文章來源:http://www.zghlxwxcb.cn/news/detail-469782.html
至于自旋鎖和讀寫鎖,使用的頻率沒有那么高,所以本喵也沒有去演示具體的代碼,有興趣的小伙伴可以去自行嘗試。文章來源地址http://www.zghlxwxcb.cn/news/detail-469782.html
到了這里,關于【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!