国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖

這篇具有很好參考價值的文章主要介紹了【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

??作者:一只大喵咪1201
??專欄:《Linux學習》
??格言:你只管努力,剩下的交給時間!
【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖

一、 信號量

之前在學習進程間通信的時候,本喵簡單的介紹過一下信號量,今天在這里進行詳細的介紹。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖
這是之前寫的基于阻塞隊列的生產(chǎn)者消費者模型中向阻塞隊列中push任務的代碼。

  • 上面代碼中有什么不足之處嗎?

一個線程在向阻塞隊列中push任務的時候,必須滿足臨界資源不滿的條件,否則就會被放入到條件變量的等待隊列中去。

但是臨界資源是否為滿是不能直接得到答案的,需要先申請鎖,然后進入臨界區(qū)訪問臨界資源去判斷它是否為滿。

  • 在判斷臨界資源是否滿足條件的過程中,必須先加鎖,再檢測,再操作,最后再解鎖。

檢測臨界資源的本質(zhì)也是在訪問臨界資源

只要對臨界資源整體加鎖,就默認現(xiàn)場會對這個臨界資源整體使用,但是實際情況可能存在:一份臨界資源,劃分為多個不同的區(qū)域,而且運行多個線程同時訪問不同的區(qū)域。

之前代碼的不足之處:

  • 在訪問臨界資源之前,無法得知臨界資源的情況。
  • 多個線程不能同時訪問臨界資源的不同區(qū)域。

1.1 概念

為此,提出了信號量來解決之前代碼的不足。

  • 信號量:本質(zhì)是一把計數(shù)器,用來衡量臨界資源中資源數(shù)量多少。
  • 申請信號量的本質(zhì):對臨界資源中特定的小塊資源的預定機制。

信號量也是一種互斥量,只要申請到信號量的線程,在未來一定能夠擁有一份臨界資源。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖
如上圖所示,將一塊臨界資源劃分為9個不同的區(qū)域,現(xiàn)在想要讓多個線程同時訪問這9個不同的區(qū)域。

  1. 創(chuàng)建一個信號量,它的值是9。
  2. 每一個來訪問臨界資源的線程都先申請信號量,也就是將計數(shù)值減一。
  3. 當計數(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)狀特性。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖

  • 環(huán)形結構起始狀態(tài)和結束狀態(tài)都是一樣的,不好判斷為空或者為滿。
  • 當環(huán)形隊列為空時,頭和尾都指向同一個位置。
  • 當環(huán)形隊列為滿時,頭和尾也都指向同一個位置。
  • 可以通過加計數(shù)器或者標記位來判滿或者空,也可以預留一個空的位置,作為滿的狀態(tài)。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖

  • 但是我們現(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)者和消費者什么情況下會訪問同一個位置?
  1. 環(huán)形隊列為空的時候,生產(chǎn)者和消費者會訪問同一個位置。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖

當隊列為空的時候,生產(chǎn)者訪問隊尾,向隊列中生產(chǎn)數(shù)據(jù),消費者訪問對首消費數(shù)據(jù),由于環(huán)形隊列且為空,所以隊首和隊尾是同一個位置。

  1. 環(huán)形隊列為滿的時候,生產(chǎn)者和消費者會訪問同一個位置。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖

當環(huán)形隊列只有一個空位置的時候,生產(chǎn)者訪問隊尾生產(chǎn)數(shù)據(jù),生產(chǎn)完畢后指向下一個位置,由于環(huán)形隊列且為滿,所以此時生產(chǎn)者又指向了隊首,和消費者訪問同一個位置。

  1. 其他任何時候,生產(chǎn)者和消費者訪問的都是不同的區(qū)域。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖

只要環(huán)形隊列不滿也不空,那么生產(chǎn)者和消費者之間都有數(shù)據(jù),它們各自訪問各自的區(qū)域。


  • 為了完成環(huán)形隊列的生產(chǎn)消費問題,必須要做的核心工作是什么?
  1. 消費者不能超過生產(chǎn)者。

【Linux學習】多線程——信號量 | 基于環(huá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ū)域。

  1. 生產(chǎn)者不能把消費者套一圈以上

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(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)任何任務,消費者線程都不用知道認為是什么。

貼圖計算任務代碼:

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖
運行結果:
【Linux學習】多線程——信號量 | 基于環(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ù)中將鎖摧毀。

這里本喵就不貼代碼了。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖
Push是向環(huán)形隊列中生產(chǎn)任務,是生產(chǎn)者在調(diào)用,所以在生產(chǎn)之前需要加鎖。Pop是從環(huán)形隊列中消費認為,是消費者在調(diào)用,所以在消費之前加鎖。

  • 互斥鎖和申請信號量誰在前比較合適呢?

如果互斥鎖在前,申請信號量在后:

  • 所有生產(chǎn)者線程或者是消費者線程都需要先競爭鎖,然后再去申請信號量,信號量申請成功才能進入臨界區(qū)。
  • 如果信號量申請失敗就抱著鎖阻塞,其他同類型線程就無法申請到鎖。

這就好比去電影院買票,必須先排隊進入放映廳才能買票。

如果申請信號量在前,互斥鎖在后:

  • 所有生產(chǎn)者線程或者消費者線程先申請信號量,再去申請鎖,然后進入臨界區(qū)。
  • 如果信號量申請失敗就不會再去申請鎖。

同樣是電影院,這就好比先買票,然后再排隊進入放映廳,沒買上票就沒必要排隊了。

對于線程來說,申請鎖也是有代價的,將信號量申請放在前面可以減少申請鎖的次數(shù),所以申請信號量在互斥鎖之前更合適。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖
創(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資源。

是什么決定著線程的等待方式呢?是需要等待的時長。

【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖

  • 當訪問臨界資源的時間較短的時候,可以使用自旋鎖,因為進入臨界區(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

到了這里,關于【Linux學習】多線程——信號量 | 基于環(huán)形隊列的生產(chǎn)者消費者模型 | 自旋鎖 | 讀寫鎖的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 【Linux C | 多線程編程】線程同步 | 信號量(無名信號量) 及其使用例子

    【Linux C | 多線程編程】線程同步 | 信號量(無名信號量) 及其使用例子

    ??博客主頁??:??https://blog.csdn.net/wkd_007?? ??博客內(nèi)容??:??嵌入式開發(fā)、Linux、C語言、C++、數(shù)據(jù)結構、音視頻?? ??本文內(nèi)容??:??介紹 ?? ??金句分享??:??你不能選擇最好的,但最好的會來選擇你——泰戈爾?? ?發(fā)布時間?: 本文未經(jīng)允許,不得轉(zhuǎn)發(fā)!?。?/p>

    2024年04月26日
    瀏覽(26)
  • Linux進程間通信【消息隊列、信號量】

    Linux進程間通信【消息隊列、信號量】

    ?個人主頁: 北 海 ??所屬專欄: Linux學習之旅 ??操作環(huán)境: CentOS 7.6 阿里云遠程服務器 在 System V 通信標準中,還有一種通信方式: 消息隊列 ,以及一種實現(xiàn)互斥的工具: 信號量 ;隨著時代的發(fā)展,這些陳舊的標準都已經(jīng)較少使用了,但作為 IPC 中的經(jīng)典知識,我們可

    2024年02月08日
    瀏覽(31)
  • 【linux】進行間通信——共享內(nèi)存+消息隊列+信號量

    【linux】進行間通信——共享內(nèi)存+消息隊列+信號量

    進程間通信方式目前我們已經(jīng)學了匿名管道,命名管道。讓兩個獨立的進程通信,前提是看到同一份資源。匿名管道適用于血緣關系的進程,一個打開寫端一個打開讀端實現(xiàn)的。命名管道適用于完全獨立的進程,打開同一份文件實現(xiàn)的。 接下來我們看看剩下的實現(xiàn)進程間通信

    2024年02月05日
    瀏覽(21)
  • 【Linux】多線程 之 POSIX信號量

    【Linux】多線程 之 POSIX信號量

    信號量又稱為 信號燈 本質(zhì)就是一個計數(shù)器,用于描述臨界資源數(shù)目的 sem: 0 - 1 - 0 若臨界資源只有1個,則sem設為1,當要使用臨界資源時,sem由1變?yōu)?,其他人在想申請,則申請不到掛起排隊,等待釋放臨界資源時 sem由0變?yōu)? ,才可以再申請臨界資源 這種信號量稱為 二元信號

    2024年02月16日
    瀏覽(47)
  • 【Linux】進程間通信 --- 管道 共享內(nèi)存 消息隊列 信號量

    【Linux】進程間通信 --- 管道 共享內(nèi)存 消息隊列 信號量

    等明年國慶去西藏洗滌靈魂,laozi不伺候這無聊的生活了 1. 通過之前的學習我們知道,每個進程都有自己獨立的內(nèi)核數(shù)據(jù)結構,例如PCB,頁表,物理內(nèi)存塊,mm_struct,所以具有獨立性的進程之間如果想要通信的話,成本一定是不低的。 2. a.數(shù)據(jù)傳輸:一個進程需要將它的數(shù)據(jù)

    2023年04月17日
    瀏覽(27)
  • 【Linux】進程間通信之共享內(nèi)存/消息隊列/信號量

    【Linux】進程間通信之共享內(nèi)存/消息隊列/信號量

    共享內(nèi)存是通過讓不同的進程看到同一個內(nèi)存塊的方式。 我們知道,每一個進程都會有對應的PCB-task_struct ,獨立的進程地址空間,然后通過頁表將地址映射到物理內(nèi)存中。此時我們就可以讓OS在內(nèi)存中申請一塊空間,然后將創(chuàng)建好的內(nèi)存空間映射到進程的地址空間中,兩個需

    2024年02月05日
    瀏覽(28)
  • 【Linux】System V 共享內(nèi)存、消息隊列、信號量

    【Linux】System V 共享內(nèi)存、消息隊列、信號量

    ?? 作者: 阿潤菜菜 ?? 專欄: Linux系統(tǒng)編程 System V 共享內(nèi)存是一種進程間通信的機制,它允許多個進程 共享一塊物理內(nèi)存區(qū)域 (稱為“段”)。System V 共享內(nèi)存的優(yōu)點是效率高,因為進程之間不需要復制數(shù)據(jù);缺點是 需要進程之間進行同步,以避免數(shù)據(jù)的不一致性 。 共

    2024年02月04日
    瀏覽(21)
  • linux中互斥鎖,自旋鎖,條件變量,信號量,與freeRTOS中的消息隊列,信號量,互斥量,事件的區(qū)別

    linux中互斥鎖,自旋鎖,條件變量,信號量,與freeRTOS中的消息隊列,信號量,互斥量,事件的區(qū)別

    對于目前主流的RTOS的任務,大部分都屬于并發(fā)的線程。 因為MCU上的資源每個任務都是共享的,可以認為是單進程多線程模型。 【freertos】003-任務基礎知識 在沒有操作系統(tǒng)的時候兩個應用程序進行消息傳遞一般使用全局變量的方式,但是如果在使用操作系統(tǒng)的應用中用全局變

    2024年02月11日
    瀏覽(26)
  • 【STM32】FreeRTOS消息隊列和信號量學習

    【STM32】FreeRTOS消息隊列和信號量學習

    一、消息隊列(queue) 隊列是一種用于實現(xiàn)任務與任務之間,任務與中斷之間消息交流的機制。 注意:1.數(shù)據(jù)的操作是FIFO模式。 2.隊列需要明確數(shù)據(jù)的大小和隊列的長度。 3.寫和讀都會出現(xiàn)堵塞。 實驗:創(chuàng)建一個消息隊列,兩個發(fā)送任務,一個接收任務。 其中任務一任務三

    2024年02月13日
    瀏覽(17)
  • 學習系統(tǒng)編程No.22【消息隊列和信號量】

    學習系統(tǒng)編程No.22【消息隊列和信號量】

    北京時間:2023/4/20/7:48,鬧鐘6點和6點30,全部錯過,根本起不來,可能是因為感冒還沒好,睡不夠吧!并且今天是星期四,這個星期這是第二篇博客,作為一個日更選手,少些了兩篇博客,充分擺爛,但是擺爛具體也是有原因的,星期一的時候莫名高燒,頭昏腦漲的感覺,睡

    2023年04月27日
    瀏覽(16)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包