目錄
寫在前面的話
什么是POSIX信號量
POSIX信號量的使用
基于環形隊列的生產消費者模型
?
寫在前面的話
? ? ? ? 本文章主要先介紹POSIX信號量,以及一些接口的使用,然后再編碼設計一個基于環形隊列的生產消費者模型來使用這些接口。
? ? ? ? 講解POSIX信號量時,首先需要對信號量有一定的了解,大家可以去看我的這一篇文章:systemV信號量,文章的前面我詳細的說明了什么是信號量以及對它的理解。
什么是POSIX信號量
????????POSIX信號量(POSIX semaphore)是一種線程同步機制,用于管理共享資源的并發訪問。POSIX信號量是基于POSIX標準定義的一組函數和數據類型,旨在提供跨平臺的線程同步能力。
????????POSIX信號量允許線程在訪問共享資源之前獲取一個信號量,通過增加或減少信號量值來控制對資源的訪問。當信號量值大于零時,線程可以獲取資源并繼續執行。當信號量值為零時,線程將被阻塞,直到其他線程釋放資源并增加信號量的值。這樣可以有效地實現對共享資源的互斥訪問和線程之間的同步。
POSIX信號量的使用
sem_init()
:用于初始化一個信號量對象。
函數原型如下:
int sem_init(sem_t *sem, int pshared, unsigned int value);
- pshared:0表示線程間共享,非零表示進程間共享
- value:信號量初始值
sem_destroy()
:用于銷毀一個信號量對象。
函數原型如下:
int sem_destroy(sem_t *sem)
sem_wait()
:嘗試獲取一個信號量,如果信號量值大于零,則將其減少并繼續執行;否則,線程將被阻塞。
函數原型如下:
int sem_wait(sem_t *sem); //P()
sem_post()
:釋放一個信號量,將其值增加,喚醒可能正在等待該信號量的線程。
函數原型如下:
int sem_post(sem_t *sem);//V()
sem_trywait()
:與?sem_wait()
?類似,但是嘗試獲取信號量時不阻塞線程,而是立即返回。sem_timedwait()
:與?sem_wait()
?類似,但是可以設置超時時間,如果在超時時間內仍無法獲取信號量,則返回錯誤。
以上兩個函數了解即可.
基于環形隊列的生產消費者模型
????????這個相當于改變了交易場所,由阻塞隊列變成了 環形隊列.
下面是利用環形隊列的幾種策略:
- 環形隊列采用數組模擬,用模運算來模擬環狀特性
- 但是上面的設計有一個問題,假設head生產,tail消費;就是當head和tail重合的時候,我們不知道到底是head == tail還是tail == head,?即不知道此時環形隊列為空還是為滿。所以可以通過加計數器或者標記位來判斷滿或者空。另外也可以預留一個空的位置,作為滿的狀態,這個原理大家可以去網上查找環形隊列的相關知識,這個置空位恰好將head和tail隔開。
- 但是現在有了信號量這個計數器,所以也能輕松地實現 線程間利用環形隊列同步的過程。?
整體代碼設計如下:
? ? ? ? 首先用類設計一個環形隊列RingQueue,并封裝一些接口push()和pop(),成員變量為一個vector數組(用數組模擬實現環形隊列)、int num_ 用來表示環形隊列的大小,c_step表示消費者的下標,p_step表示生產者的下標,然后有兩個信號量space_sem_和data_sem_,分別表示空間資源信號量和數據資源信號量。一開始的時候沒有數據,所有空間都可使用,所以我們將空間資源信號量space_sem_設置為環形隊列的長度n,data_sem_設置為0,表示沒有數據。
? ? ? ? 信號量的數據類型為sem_t,但是為了方便,我對信號量進行了一個封裝,類Sem,里面包含了信號量的初始化,p()和v()操作等,然后上面兩個信號量的類型就直接使用Sem.
然后在RingQueue類中,兩個接口push和pop的實現邏輯如下:
- push():這是生產者所使用的,生產者關注的是空間資源,如果有空間就生產,沒有就停止。?生產后空間資源信號量space_sem_-1,但是數據資源信號量data_sem_+1,然后每次就在對應位置上寫入相應的數據即可,記得模上數組的長度,因為邏輯結構是一個環形隊列。
- pop():這是消費者所使用的,消費者關注的是數據資源,有數據就消費,沒數據就不能消費。消費后數據資源信號量data_sem_-1,但是空間資源信號量space_sem+1,同樣地將對應位置的數據取出即可。
所以環形隊列RingQueue類和Sem類的代碼如下:
Ringqueue.hpp類
#pragma once #include<iostream> #include<pthread.h> #include<vector> #include "Sem.hpp" using namespace std;const int g_default_num = 5;template<class T> class RingQueue { public://對環形隊列進行初始化RingQueue(int default_num = g_default_num):ring_queue_(g_default_num),num_(g_default_num),c_step(0),p_step(0),space_sem_(default_num),data_sem_(0){}~RingQueue(){}//生產者:關注空間資源void push(const T& in){space_sem_.p();ring_queue_[p_step++] = in;p_step %= num_;data_sem_.v();}//消費者:關注數據資源void pop(T* out){data_sem_.p();*out = ring_queue_[c_step++];c_step %= num_;space_sem_.v();}private:vector<T> ring_queue_;int num_;int c_step;//消費者下標int p_step;//生產者下標Sem space_sem_;Sem data_sem_; };
Sem.hpp類
#pragma once #include "ringQueue.hpp" #include <semaphore.h>class Sem { public:Sem(int val){sem_init(&sem_,0,val);}void p(){sem_wait(&sem_);}void v(){sem_post(&sem_);}~Sem(){sem_destroy(&sem_);} private:sem_t sem_; };
然后我們對代碼進行測試,測試代碼如下,和上一節的測試代碼幾乎一樣:
#include "ringQueue.hpp" #include<sys/types.h> #include<unistd.h> #include <time.h> using namespace std; void* consumer(void* args) {RingQueue<int>* rq = (RingQueue<int>*)args;while(true){int x;//1.從環形隊列中拿取數據rq->pop(&x);//2.進行一定的處理cout << "消費: " << x << endl; } } void* producter(void* args) {RingQueue<int>* rq = (RingQueue<int>*)args;while ((true)){//1.構建數據或任務對象 -- 一般可以從外部來,不要忽略時間消耗問題int x = rand() % 100 + 1;cout << "生產: " << x << endl; //2.推送到環形隊列中rq->push(x);//完成生產的過程// sleep(1);}}int main() {srand((unsigned int)time(nullptr) ^ getpid() ^ 12366 );RingQueue<int>* rq = new RingQueue<int>();pthread_t c,p;// rq->debug();pthread_create(&c,nullptr,consumer,(void*)rq);pthread_create(&p,nullptr,producter,(void*)rq);pthread_join(c,nullptr);pthread_join(p,nullptr);return 0; }
代碼也成功的執行了:
?
? ? ? ? 上面是單線程,即單生產者,單消費者。
????????我們也改成多線程并發執行的,這也是生產消費者模型的意義所在。
????????當多線程并發執行時,如果兩個線程 同時訪問臨界資源可能出錯,所以需要在臨界資源前后加上鎖,使得只能有一個線程可以訪問臨界資源。
? ? ? ? 但是這樣和單線程有什么區別啊,都是單線程訪問,多線程的意義在哪里?
? ? ? ? 首先不要狹隘的認為,把任務或數據放在交易場所就是生產和消費了。將數據或任務拿到之后的處理,才是最耗費時間的,雖然拿的時候是加鎖一個個拿的,但是處理的時候,卻是一起處理的!所以生產消費者模型主要意義體現在可以并發的處理任務。
- 生產的本質:私有的任務 -> 公共空間中
- 消費的本質:公共空間中 -> 私有的
信號量本質是一把計數器 那計數器的意義是什么?
????????可以不用進入臨界區,就可以得知資源的情況,甚至可以減少臨界區內部的判斷。
申請鎖 -> 判斷臨界資源和訪問 -> 釋放鎖 ---> 本質我們并不清楚臨界資源的情況,信號量要提前預設資源的情況,而且在pv變化中,我們在外部就可以知道臨界資源的情況.
? ? ? ? 所以我們在RingQueue類中,加入兩個鎖,分別為生產者和消費者:
?主函數中,創建多個線程:
?
?運行后,可以發現不同的線程生產和消費任務了。
這便是本章的全部內容了,主要講述了POSIX信號量,即基于環形隊列的生產消費者模型的一個實現。