「前言」文章是關(guān)于Linux多線程方面的知識,上一篇是?Linux多線程詳解(四),今天這篇是 Linux多線程詳解(五),內(nèi)容大致是信號量,講解下面開始!
「歸屬專欄」Linux系統(tǒng)編程
「主頁鏈接」個人主頁
「筆者」楓葉先生(fy)
「楓葉先生有點文青病」「每篇一句」
求其上,得其中;
求其中,得其下;
求其下,必敗。
——《孫子兵法》
十、POSIX信號量
10.1 分析之前代碼的不足
在進(jìn)入話題正文之前先談三個概念:串行、并行、并發(fā)
- 并發(fā):并發(fā)是指同時處理多個任務(wù),并且這些任務(wù)可能會相互影響,需要協(xié)調(diào)和管理。例如,在一個多用戶系統(tǒng)中,多個用戶可能同時訪問同一個資源,需要通過并發(fā)控制來避免沖突和競爭
- 并行:并行是指同時執(zhí)行多個任務(wù),每個任務(wù)都在不同的處理器上執(zhí)行,相互之間獨立,不會相互影響。例如,在一個多處理器系統(tǒng)中,多個任務(wù)可以同時在不同的處理器上執(zhí)行,提高系統(tǒng)的處理能力和效率
- 串行:串行是指按照一定的順序依次執(zhí)行任務(wù),每個任務(wù)必須在前一個任務(wù)完成后才能開始執(zhí)行。在串行執(zhí)行中,每個任務(wù)都是獨占CPU資源的,直到該任務(wù)執(zhí)行完畢后,才會釋放CPU資源給下一個任務(wù)使用
下面進(jìn)入正文,代碼樣例是上一篇的Linux多線程(四)的代碼
- 一個線程在操作臨界資源的時候,一定是要滿足臨界資源的條件的
- 可是,是否滿足條件,我們是無法直接得知的(不能事先得知,在沒有訪問臨界資源之前,是無法得知是否滿足條件)
- 只能先進(jìn)行加鎖,再檢測,再操作,再解鎖。根據(jù)檢測的結(jié)果決定線程下一步怎么走
- 進(jìn)行檢測也是在訪問臨界資源
- 比如上面的代碼樣例中,線程是無法知道是否滿足生產(chǎn)條件的,線程必須申請到鎖,進(jìn)入臨界區(qū)才能檢測是否滿足條件,根據(jù)檢測的結(jié)果決定線程下一步怎么走
注意:只要我們對臨界資源進(jìn)行加鎖,就默認(rèn)了對這個資源進(jìn)行整體使用
但實際情況可能存在:一份臨界資源,允許被不同線程同時訪問不同的區(qū)域(把一份共享資源分成許多份使用)?,可以進(jìn)行這樣操作就是信號量?
10.2 信號量概念
信號量主要用于線程同步和互斥的
POSIX信號量和SystemV信號量作用相同,都是用于同步和互斥操作,達(dá)到無沖突的訪問共享資源目的,?但POSIX可以用于線程間同步和互斥
信號量:semaphore
信號量的本質(zhì):
信號量(信號燈)本質(zhì)是一個計數(shù)器,是描述臨界資源中資源數(shù)目的計數(shù)器,信號量能夠更細(xì)粒度的對臨界資源進(jìn)行管理
信號量解釋:
- 以例子進(jìn)行解釋,比如一個電影院,這個電影院看作是臨界資源,電影院里面的每一個座位看作是臨界資源劃分成的一個個子資源;
- 我只有買了票,這個座位在一定時間內(nèi)才是歸屬我的,即便我買了票不去,這個座位也是我的,反過來,你沒買票,這個座位就一定不是你的。買的票一定對應(yīng)著一個座位。
- 同樣道理,我想用某種資源的時候,可以對這種資源進(jìn)行預(yù)訂,就相當(dāng)于買票,買了票我就已經(jīng)預(yù)訂了這個座位,我預(yù)訂了這個資源,它在未來的某一時間一定屬于我
- 假設(shè)電影院的座位只有100個,你不能賣出第101張票吧,信號量就相當(dāng)于這里的票一樣,票賣完了就不允許再賣了,再賣就會發(fā)生 ‘座位沖突’,也就對應(yīng)進(jìn)程訪問該資源會發(fā)生沖突
- 線程想要訪問某個臨界資源的條件是:申請到信號量,申請到了信號量就相當(dāng)于預(yù)訂了臨界資源的某一部分小資源,就允許該線程對該資源的訪問
- 將一塊臨界資源拆分成一個個的子資源,不同的線程可以對不同的子資源進(jìn)行訪問,從而實現(xiàn)并發(fā)(程序員編碼保證不同的線程可以并發(fā)訪問臨界資源的不同區(qū)域)
而申請信號量的前提是:所有線程要看到一個同一個信號量,所以信號量本身就是臨界資源
- 既然信號量是臨界資源,是臨界資源就要保證自身的安全性。
- 信號量的本質(zhì)是一個計數(shù)器,計數(shù)器的操作只有 ++(遞增)和 --(遞減)
- 進(jìn)行 ++ 和 -- 操作就要保證信號量自身的安全性,所以這個計數(shù)器就不能只是單純整型int,因為這樣的 ++ 和 -- 操作不是原子性的
- 所以Linux對信號量進(jìn)行封裝了一種類型 sem_t,這種類型可以保證 ++ 和 -- 操作是原子性的
只要申請到了信號量,在未來就一定可以擁有該臨界資源的一部分。
申請信號量的本質(zhì):對臨界資源中特定小塊資源進(jìn)行預(yù)訂(申請信號量就是一種預(yù)訂機制)
信號量的核心操作是 ++ 和 --(PV操作)
- 對信號量++,歸還信號量資源(V操作)
- 對信號量--,申請信號量資源(P操作)
信號量這種預(yù)訂機制,就有可能我們在真正訪問臨界資源之前,我們就可以提前知道臨界資源的使用情況
- 只要申請信號量成功,臨界資源就一定會有一部分資源是你的
- 只要申請信號量失敗,就說明條件不就緒(不滿足),你只能進(jìn)行等待
- 所以,就不需要像互斥鎖,只有加鎖后進(jìn)行判斷才能知道臨界資源的使用情況
提前知道臨界資源的使用情況:類比上面的電影院(臨界資源),有一場電影只有100張票(臨界資源分成100個小資源),只賣了80張,每張電影票對應(yīng)著一個唯一的座位,電影還沒有開始我們就已經(jīng)預(yù)知了該電影院資源(座位)的使用情況
10.3 POSIX信號量函數(shù)
初始化信號量
初始化信號量的函數(shù)叫做 sem_init,man 3 sem_init 查看
函數(shù):sem_init
頭文件:#include <semaphore.h>
函數(shù)原型:
int sem_init(sem_t *sem, int pshared, unsigned int value);
參數(shù):
第一個參數(shù)sem:需要初始化的信號量
第二個參數(shù)pshare:0表示線程間共享,非零表示進(jìn)程間共享
第三個參數(shù)value:信號量初始值(信號量初始值)
返回值:
初始化信號量成功返回0,失敗返回-1,錯誤碼被設(shè)置
?銷毀信號量
銷毀信號量的函數(shù)叫做 sem_destroy,?man 3 sem_destroy查看
函數(shù):sem_destroy
頭文件:#include <semaphore.h>
函數(shù)原型:
int sem_destroy(sem_t *sem);
參數(shù):
參數(shù)sem:需要銷毀的信號量
返回值:
銷毀信號量成功返回0,失敗返回-1,錯誤碼被設(shè)置
等待信號量(申請信號量)(P操作)
等待信號量的函數(shù)叫做 sem_wait,man 3 sem_wait查看
函數(shù):sem_wait
頭文件:#include <semaphore.h>
函數(shù)原型:
int sem_wait(sem_t *sem);
參數(shù):
參數(shù)sem:需要等待的信號量
返回值:
等待信號量成功返回0,信號量的值-1
等待信號量失敗返回-1,錯誤碼被設(shè)置,信號量的值保持不變
?發(fā)布信號量(釋放信號量)(V操作)
發(fā)布信號量的函數(shù)叫做 sem_post,man 3 sem_post查看
函數(shù):sem_post
頭文件:#include <semaphore.h>
函數(shù)原型:
int sem_post(sem_t *sem);
參數(shù):
參數(shù)sem:需要發(fā)布的信號量
返回值:
等待信號量成功返回0,信號量的值+1
等待信號量失敗返回-1,錯誤碼被設(shè)置,信號量的值保持不變
注意:信號量為1時,說明臨界資源是一整個整體使用的(信號量所描述的臨界資源只有一份),提供互斥功能(此時信號量的作用基本等價于互斥鎖),提供互斥功能的信號量也叫二元信號量
十一、基于環(huán)形隊列的生產(chǎn)消費模型
11.1 環(huán)形隊列
環(huán)形隊列本質(zhì)上是一個數(shù)組,環(huán)形隊列是固定大小的,在數(shù)據(jù)結(jié)構(gòu)學(xué)過,不解釋
環(huán)形隊列的判空判滿問題:(1)計數(shù)器,(2)空出最后一個位置
11.2??環(huán)形隊列的生產(chǎn)消費模型
以但單生產(chǎn)單消費為例,生產(chǎn)者和消費者在這兩種情況下可能會訪問同一個位置:(1)環(huán)形隊列空的時候,(2)環(huán)形隊列滿的時候,對于其他情況生產(chǎn)者和消費者訪問的位置都不一樣
環(huán)形隊列的生產(chǎn)消費模型,必須要滿足兩個條件:
- 消費者不能超過生產(chǎn)者
- 無論是生產(chǎn)者還是消費者,不允許將對方套一個圈以上
對于生產(chǎn)者和消費者來說,它們關(guān)注的資源是不同的:
- 生產(chǎn)者關(guān)注的是環(huán)形隊列當(dāng)中是否有空間,只要有空間生產(chǎn)者就可以進(jìn)行生產(chǎn)。
- 消費者關(guān)注的是環(huán)形隊列當(dāng)中是否有數(shù)據(jù),只要有數(shù)據(jù)消費者就可以進(jìn)行消費。
這里就需要用信號量來描述環(huán)形隊列當(dāng)中的空間資源和數(shù)據(jù)資源,需要創(chuàng)建兩個信號量,假設(shè)是A,B,A信號量用來標(biāo)識空間資源的多少,B信號量用于標(biāo)識數(shù)據(jù)資源的多少,并且這兩個信號量初始化的值不同:
- A信號量初始值我們應(yīng)該設(shè)置為環(huán)形隊列的容量,因為剛開始時環(huán)形隊列當(dāng)中全是空的,空間資源為環(huán)形隊列的容量
- B信號量初始值我們應(yīng)該設(shè)置為0,因為剛開始時環(huán)形隊列當(dāng)中沒有數(shù)據(jù)
生產(chǎn)者申請空間資源,釋放數(shù)據(jù)資源
- 對于生產(chǎn)者來說,生產(chǎn)者每次生產(chǎn)數(shù)據(jù)前都需要先申請(等待)A信號量,
- 如果A信號量的值不為0,則信號量申請成功,A信號量-1(即等待A信號量成功,P操作),此時生產(chǎn)者可以進(jìn)行生產(chǎn)操作。
- 如果A信號量的值為0,則信號量申請失敗,此時生產(chǎn)者需要在A信號量的等待隊列下進(jìn)行阻塞等待,直到環(huán)形隊列當(dāng)中有新的空間后再被喚醒
當(dāng)生產(chǎn)者生產(chǎn)完數(shù)據(jù)后,應(yīng)該釋放B信號量,因為此時隊列中的數(shù)據(jù)已經(jīng)+1,B信號量也需要+1(即釋放(發(fā)布)B信號量,V操作)
消費者申請數(shù)據(jù)資源,釋放空間資源?
- 對于消費者來說,消費者每次消費數(shù)據(jù)前都需要先申請(等待)B信號量,
- 如果B信號量的值不為0,則信號量申請成功,B信號量-1(即等待A信號量成功,P操作),此時消費者可以進(jìn)行消費操作。
- 如果B信號量的值為0,則信號量申請失敗,此時消費者需要在B信號量的等待隊列下進(jìn)行阻塞等待,直到環(huán)形隊列當(dāng)中有新的數(shù)據(jù)后再被喚醒
當(dāng)消費者消費完數(shù)據(jù)后,應(yīng)該釋放A信號量,因為此時隊列中的數(shù)據(jù)已經(jīng)-1,空出一個空間資源,A信號量也需要+1(即釋放(發(fā)布)A信號量,V操作)
下面進(jìn)行編寫代碼?
11.3 代碼實現(xiàn)
Linux多線程(四)實現(xiàn)的是是基于queue的生產(chǎn)者消費者模型,其空間可以動態(tài)分配(采用互斥鎖+條件變量實現(xiàn));現(xiàn)在基于固定大小的環(huán)形隊列重寫這個生產(chǎn)者消費者模型(采用POSIX信號量實現(xiàn))
為了方便理解,實現(xiàn)的代碼也是單生產(chǎn)者,單消費者
RingQueue.hpp
用C++STL庫當(dāng)中的vector模擬實現(xiàn)環(huán)形隊列?
#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <cassert>
static const int gcap = 5;
template <class T>
class RingQueue
{
private:
// P操作,等待信號量
void P(sem_t &sem)
{
int n = sem_wait(&sem);
assert(n == 0);
(void)n;
}
// V操作,發(fā)布信號量
void V(sem_t &sem)
{
int n = sem_post(&sem);
assert(n == 0);
(void)n;
}
public:
RingQueue(const int &cap = gcap) : _ringQueue(cap), _cap(cap)
{
sem_init(&_spaceSem, 0, _cap);
sem_init(&_dataSem, 0, 0);
_productorStep = _consumerStep = 0;
}
// 生產(chǎn)者生產(chǎn)數(shù)據(jù)
void Push(const T &in)
{
// 申請信號量,申請成功繼續(xù)執(zhí)行代碼,失敗阻塞等待
P(_spaceSem);
_ringQueue[_productorStep] = in;
_productorStep++;
_productorStep %= _cap;
// 此時數(shù)據(jù)+1,發(fā)布_dataSem信號量
V(_dataSem);
}
// 消費者消費數(shù)據(jù)
void Pop(T *out)
{
// 申請信號量,申請成功繼續(xù)執(zhí)行代碼,失敗阻塞等待
P(_dataSem);
*out = _ringQueue[_consumerStep];
_consumerStep++;
_consumerStep %= _cap;
// 此時數(shù)據(jù)-1,發(fā)布_spaceSem信號量
V(_spaceSem);
}
~RingQueue()
{
sem_destroy(&_spaceSem);
sem_destroy(&_dataSem);
}
private:
std::vector<T> _ringQueue;
int _cap;
sem_t _spaceSem; // 空間資源
sem_t _dataSem; // 數(shù)據(jù)資源
int _productorStep; // 生產(chǎn)者的下標(biāo),共用_ringQueue,兩套下標(biāo)
int _consumerStep; // 消費者的下標(biāo),共用_ringQueue
};
Main.cc?
主函數(shù)只需要創(chuàng)建一個生產(chǎn)者線程和一個消費者線程,生產(chǎn)者線程不斷生產(chǎn)數(shù)據(jù)放入環(huán)形隊列,消費者線程不斷從環(huán)形隊列里取出數(shù)據(jù)進(jìn)行消費?
#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>
using namespace std;
void *consumer(void *ringqueue)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(ringqueue);
while (true)
{
int data;
rq->Pop(&data);
cout << "消費完成,消費的數(shù)據(jù)是:" << data << endl;
sleep(1);
}
return nullptr;
}
void *productor(void *ringqueue)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(ringqueue);
while (true)
{
int data = rand() % 10 + 1;
rq->Push(data);
cout << "生產(chǎn)完成,生產(chǎn)的數(shù)據(jù)是:" << data << endl;
sleep(1);
}
return nullptr;
}
int main()
{
srand((unsigned int)time(nullptr) ^ 0x4589315); // 用于獲取隨機數(shù)
RingQueue<int> *rq = new RingQueue<int>();
// c:consumer p:productor
pthread_t c, p;
pthread_create(&c, nullptr, consumer, rq);
pthread_create(&p, nullptr, productor, rq);
// 線程等待
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete rq;
return 0;
}
?生產(chǎn)者消費者步調(diào)一致
生產(chǎn)者每隔一秒生產(chǎn)一個數(shù)據(jù),而消費者是每隔一秒消費一個數(shù)據(jù),因此運行代碼后我們可以看到生產(chǎn)者和消費者的執(zhí)行步調(diào)是一致的
測試運行結(jié)果
生產(chǎn)者生產(chǎn)快,消費者消費慢
修改代碼,生產(chǎn)者生產(chǎn)快,消費者消費慢
測試運行結(jié)果
- 第一條打印是因為兩個線程同時執(zhí)行,消費者線程消費了一次數(shù)據(jù)才sleep(1)
- 此時由于生產(chǎn)者生產(chǎn)的很快,運行代碼后一瞬間生產(chǎn)者就將環(huán)形隊列打滿了,此時生產(chǎn)者想要再進(jìn)行生產(chǎn),但空間資源已經(jīng)為0了,生產(chǎn)者只能阻塞等待空間資源
- 消費者消費一個環(huán)形隊列里面的數(shù)據(jù),生產(chǎn)者又會生產(chǎn)一個數(shù)據(jù),因此后續(xù)生產(chǎn)者和消費者的步調(diào)又變成一致的了
?
生產(chǎn)者生產(chǎn)慢,消費者消費快
修改代碼,生產(chǎn)者生產(chǎn)慢,消費者消費快
測試運行
- 第一條打印是因為兩個線程同時執(zhí)行,生產(chǎn)者線程生產(chǎn)了一個數(shù)據(jù)才sleep(1)
- 此時由于消費者消費的很快,運行代碼由于環(huán)形隊列中沒有數(shù)據(jù),只能阻塞等待生產(chǎn)者生產(chǎn)數(shù)據(jù)
- 生產(chǎn)者生產(chǎn)一個數(shù)據(jù),消費者又會消費一個數(shù)據(jù),因此后續(xù)生產(chǎn)者和消費者的步調(diào)又變成一致的了
文章來源:http://www.zghlxwxcb.cn/news/detail-471622.html
--------------------- END ----------------------文章來源地址http://www.zghlxwxcb.cn/news/detail-471622.html
「 作者 」 楓葉先生
「 更新 」 2023.6.3
「 聲明 」 余之才疏學(xué)淺,故所撰文疏漏難免,
或有謬誤或不準(zhǔn)確之處,敬請讀者批評指正。
到了這里,關(guān)于『Linux』第九講:Linux多線程詳解(五)_ 信號量的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!