Linux:生產者消費者模型(Posix信號量)


應用場景

有線程不斷的生產數據,有線程不斷的處理數據

數據的生產與數據的處理,放在同一個線程中完成,因爲執行流只有一個,那麼肯定是生產一個處理一個,處理完一個後才能生產一個

  • 這樣的話依賴關係太強 - 如果處理比較慢,也會拖的生產速度慢下來
  • 因此爲了提高效率,將生產與處理放到不同的執行流中完成,中間增加一個數據緩衝區,作爲中間的數據緩衝場所

概念

生產者消費者模式就是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊, 而通過阻塞隊列來進行通訊,所以生產者生產完數據之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生產者要數據,而是直接從阻塞隊列裏取,阻塞隊列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。這個阻塞隊列就是用來給生產者和消費者解耦的。

優點:

  • 解耦合 (生產是一個功能/處理也是一個功能。如果放到一起,相互受到的影響就比較大; 解耦合,將各個功能分離開來,降低相互影響的程度)
  • 支持併發
  • 支持忙閒不均

併發:輪詢處理(任務一個一個處理)
並行:同時處理(cpu資源多的情況可以支持)
而這裏的支持併發:指的是可以有多個執行流處理

在這裏插入圖片描述

基於BlockingQueue的生產者消費者模型

在多線程編程中阻塞隊列(Blocking Queue)是一種常用於實現生產者和消費者模型的數據結構。其與普通的隊列區別在於:

  • 當隊列爲空時,從隊列獲取元素的操作將會被阻塞,直到隊列中被放入了元素
  • 當隊列滿時,往隊列裏存放元素的操作也會被阻塞,直到有元素被從隊列中取出

以上的操作都是基於不同的線程來說的,線程在對阻塞隊列進程操作時會被阻塞

在這裏插入圖片描述

實現:

  • 一個場所(線程安全的緩衝區)- (數據隊列)
  • 兩種角色(生產者與消費者)
  • 三種關係(生產者與生產者之間、消費者與消費者之間是互斥關係,生產者與消費者之間是同步與互斥關係)-(實現線程安全)

生產者與消費者,其實只是兩種業務處理的線程而已:
創建線程就可以

實現的關鍵在於線程安全的隊列:
封裝一個線程安全的BlockQueue - 阻塞隊列 - 向外提供線程安全的入隊/出隊操作

模版:

Class BlockQueue{
public:
    BlockQueue();
    // 編碼風格: 純輸入參數 const int & / 輸出型參數 指針 / 輸入輸出型 &
    bool Push(int data);    // 入隊數據
    bool Pop(int *data);    // 出隊數據
private:
    std::queue<int> _queue;    // STL中queue容器,是非線程安全的 - 因爲STL設計之初就是奔着性能去的(功能多了,耦合度就高了)
    int _capacity;    // 隊列中節點的最大數量(數據也不能無限制添加,內存耗盡程序就崩潰了)
    pthread_mutex_t _mutex;
    pthread_cond_t _productor_cond;    // 生產者隊列
    pthread_cond_t _customer_cond;    // 消費者隊列
}

代碼示例:

#include <iostream>
#include <cstdio>
#include <queue>
#include <pthread.h>
using namespace std;

#define QUEUE_MAX 5

// 線程安全的阻塞隊列 - 沒有數據則消費者阻塞 / 數據滿了則生產者阻塞
class BlockQueue{
public:
	BlockQueue(int maxq = QUEUE_MAX):_capacity(maxq){
		pthread_mutex_init(&_mutex, NULL);
		pthread_cond_init(&_pro_cond, NULL);
		pthread_cond_init(&_cus_cond, NULL);
	}
	~BlockQueue(){
		pthread_mutex_destroy(&_mutex);
		pthread_cond_destroy(&_pro_cond);
		pthread_cond_destroy(&_cus_cond);
	}
	bool Push(int data){
		// 生產者纔會入隊數據,如果隊列中數據滿了則需要阻塞
		pthread_mutex_lock(&_mutex);

		// _queue.size() 獲取隊列節點個數
		while(_queue.size() == _capacity){
			pthread_cond_wait(&_pro_cond, &_mutex);
		}
		_queue.push(data);	// _queue.push()入隊操作
		pthread_mutex_unlock(&_mutex);	// 解鎖
		pthread_cond_signal(&_cus_cond);	// 喚醒消費者

		return true;
	}

	// 使用指針,表示這是一個輸出型數據,用於返回數據
	bool Pop (int *data){
		// 出隊都是消費者,有數據才能出隊,沒有數據要阻塞
		pthread_mutex_lock(&_mutex);

		//_queue.empty()  若queue爲NULL,則返回true
		while(_queue.empty()){
			pthread_cond_wait(&_cus_cond, &_mutex);
		}
		*data = _queue.front();	// _queue.front() 獲取隊首節點數據
		_queue.pop();	// 出隊
		pthread_mutex_unlock(&_mutex);
		pthread_cond_signal(&_pro_cond);

		return true;
	}
private:
	std::queue<int> _queue;
	int _capacity;

	pthread_mutex_t _mutex;
	pthread_cond_t _pro_cond;
	pthread_cond_t _cus_cond;
};

void *thr_productor(void *arg){
	BlockQueue *queue = (BlockQueue*)arg;
	int i = 0;
	while(1){
		// 生產者不斷生產數據
		queue->Push(i);
		printf("productor push data:%d\n", i++);
	}
	return NULL;
}

void * thr_customer(void *arg){
	BlockQueue *queue = (BlockQueue*)arg;
	while(1){
		// 消費者不斷獲取數據進行處理
		int data;
		queue->Pop(&data);
		printf("customer pop data:%d\n", data);
	}
	return NULL;
}

int main(){
	int ret, i;
	pthread_t ptid[4], ctid[4];
	BlockQueue queue;

	for(i = 0; i < 4; i++){
		ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&queue);
		if(ret != 0){
			printf("create productor thread error\n");
			return -1;
		}
		ret = pthread_create(&ctid[i], NULL, thr_customer, (void*)&queue);
		if(ret != 0){
			printf("create customer thread error\n");
			return -1;
		}
	}
	
	for(i = 0; i < 4; i++){
		pthread_join(ptid[i], NULL);
		pthread_join(ctid[i], NULL);
	}
	return 0;
}

一次生成結果:

productor push data:79517
productor push data:79518
productor push data:79519
productor push data:79520
productor push data:79521
customer pop data:79517
customer pop data:79518
customer pop data:79519
customer pop data:79520
customer pop data:79521
productor push data:75501
productor push data:75502
productor push data:75503
productor push data:75504
productor push data:75505
customer pop data:75501
customer pop data:75502
customer pop data:75503
customer pop data:75504
customer pop data:75505
...
productor push data:79522
productor push data:79523
productor push data:79524
productor push data:79525
productor push data:79526
customer pop data:79522
customer pop data:79523
customer pop data:79524
customer pop data:79525
customer pop data:79526
productor push data:75506
productor push data:75507
productor push data:75508
productor push data:75509
productor push data:75510
customer pop data:75506
customer pop data:75507
customer pop data:75508
customer pop data:75509
customer pop data:75510

posix信號量概念

信號量: 可以用於實現進程/線程間同步與互斥(主要用於實現同步)

本質就是一個計數器+pcb等待隊列

  • 同步的實現

通過自身的計數器對資源進行計數,並且通過計數器的資源計數,判斷進程/線程是否能夠符合訪問資源的條件:
若不符合則調用提供的接口使進程/線程阻塞;等到其他進程/線程促使條件滿足之後,可以喚醒pcb等待隊列上的pcb

  • 互斥的實現

保證計數器的計數不大於1,就保證資源只有一個,同一時間只有一個進程/線程能夠訪問資源,實現互斥

與sustem v的區別

之前我們學到過system v版本的進程間通信中也有syetem v版本的信號量,那麼它與posix的信號量有什麼區別呢?

  • 一般來說System V版本的進程間通信用於進程
  • POSIX版本的進程間通信用於線程。他們的區別主要在於信號量和共享內存

信號量的區別:
system v版本的信號量一般是隨內核的,無論有無競爭都要執行系統調用,所以性能上比較差。它的接口是semget,semctl,semop。

posix版本的信號量同時支持無命名的信號量和有命名的信號量。它在無競爭的時候是不陷入內核的。所以效率更高。

  • 無命名的信號量一般用於線程同步,它的生命週期是隨進程的,它的主要接口有 sem_init,sem_destory,sem_wait,sem_post。
  • 有命名的信號量一般用於進程同步,一般有一個文件關聯他們,有命名的信號量是隨內核的,它的主要接口有 sem_open,sem_close,sem_unlink。

代碼操作

  1. 定義信號量: sem_t
  2. 初始化信號量:
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value); 

參數:

  • sem: 定義的信號量變量
  • pshared:
    0表示線程間共享 - (全局變量計數器)
    非0表示進程間共享 - (申請一塊共享內存,在共享內存裏面實現一個pcb等待隊列、計數器)
  • value: 信號量初始值 - 初始資源有多少計數就是多少

返回值:

  • 成功返回 0 ;失敗返回-1
  1. 在訪問臨界資源之前,先訪問信號量,判斷是否能夠訪問:會將信號量的值減1
int sem_wait(sem_t *sem); - 通過自身計數判斷是否滿足訪問條件,不滿足則一直阻塞線程/進程
int sem_trywait(sem_t *sem); - 通過自身計數判斷是否滿足訪問條件,不滿足則立即報錯返回,EINAVL     
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); - 通過自身計數判斷是否滿足訪問條件,不滿足則等待指定時間,超時後報錯返回 - ETIMIDOUT

  1. 促使訪問條件滿足+1,喚醒阻塞線程/進程
int sem_post(sem_t *sem);

功能:

  • 通過信號量喚醒自己阻塞隊列上的pcb
  1. 銷燬信號量
int sem_destroy(sem_t *sem);

第一個消費者的例子是基於queue的,其空間可以動態分配,現在基於固定大小的環形隊列重寫這個程序 (POSIX信號量)

基於環形隊列的生產消費模型

在這裏插入圖片描述
模版

class RingQueue{
    std::vector<int> _queue;
    int _capacity;    // 這是隊列的容量
    int _step_read;    // 獲取數據的位置下標
    int _step_write;    // 寫入數據的位置下標

    sem_t _lock;    // 這個信號量用於實現互斥
    sem_t _sem_idle;    // 這個信號量用於對空閒空間進行計數 - 對於生產者來說有空閒空間計數>0的時候才能寫數據 - 初始爲節點個數
    sem_t _sem_data;    // 這個信號量用於對具有數據的空間進行計數  - 對於消費者來說有數據的空間技術>0的時候才能取出數據 - 初始爲0
    
};

代碼示例

#include <iostream>
#include <cstdio>
#include <vector>
#include <pthread.h>
#include <semaphore.h>

#define QUEUE_MAX 5

class RingQueue{
public:
	RingQueue(int maxq = QUEUE_MAX):
		_queue(maxq), _capacity(maxq),
		_step_read(0), _step_write(0)
	{
		//sem_init(信號量, 進程/線程的標誌,信號量初值)
		sem_init(&_lock, 0, 1);	// 用於實現互斥鎖
		sem_init(&_sem_data, 0, 0);	// 數據空間計數初始爲0
		sem_init(&_sem_idle, 0, maxq);	// 空閒空間計數初始爲節點個數

	}
	~RingQueue(){
		sem_destroy(&_lock);
		sem_destroy(&_sem_data);
		sem_destroy(&_sem_idle);
	}
	bool Push(int data){
		// 1.判斷是否能夠訪問資源,不能訪問則阻塞
		sem_wait(&_sem_idle);	// 空閒空間計數的判斷(能訪問,則空閒空間計數-1)
		// 2.能訪問,則加鎖,保護訪問過程 
		sem_wait(&_lock);	// lock計數不大於1(當前若可以訪問則-1,別人就不能訪問了)
		// 3.資源的訪問
		_queue[_step_write] = data;
		_step_write = (_step_write + 1) % _capacity;	// 走到最後,從頭開始
		// 4.解鎖
		sem_post(&_lock);	// lock計數+1,喚醒其他因爲加鎖阻塞的線程
		// 5.入隊數據之後,數據空間計數+1,喚醒消費者
		sem_post(&_sem_data);
		return true;
	}
	bool Pop(int *data){
		sem_wait(&_sem_data);	// 有沒有數據
		sem_wait(&_lock);	// 有數據,則加鎖,保護訪問數據的過程
		*data = _queue[_step_read];	// 獲取數據
		_step_read = (_step_read + 1) % _capacity;
		sem_post(&_lock);	// 解鎖操作
		sem_post(&_sem_idle);	// 取出數據,則空閒空間計數+1,喚醒生產者
		return true;
	}

private:
    std::vector<int> _queue;	// 數組,vector需要初始化節點數量
    int _capacity;	// 這是隊列的容量
    int _step_read;	// 獲取數據的位置下標
    int _step_write;// 寫入數據的位置下標

    sem_t _lock;		// 這個信號量用於實現互斥
    sem_t _sem_idle;// 這個信號量用於對空閒空間進行計數 - 對於生產者來說有空閒空間計數>0的時候才能寫數據 - 初始爲節點個數
    sem_t _sem_data;// 這個信號量用於對具有數據的空間進行計數  - 對於消費者來說有數據的空間技術>0的時候才能取出數據 - 初始爲0
};

void *thr_productor(void *arg){
	RingQueue *queue = (RingQueue*)arg;
	int i = 0;
	while(1){
		// 生產者不斷生產數據
		queue->Push(i);
		printf("tid:%p productor push data:%d\n", pthread_self(), i++);
	}
	return NULL;
}

void * thr_customer(void *arg){
	RingQueue *queue = (RingQueue*)arg;
	while(1){
		// 消費者不斷獲取數據進行處理
		int data;
		queue->Pop(&data);
		printf("customer pop data:%d\n", data);
	}
	return NULL;
}

int main(){
	int ret, i;
	pthread_t ptid[4], ctid[4];
	RingQueue queue;

	for(i = 0; i < 4; i++){
		ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&queue);
		if(ret != 0){
			printf("create productor thread error\n");
			return -1;
		}
		ret = pthread_create(&ctid[i], NULL, thr_customer, (void*)&queue);
		if(ret != 0){
			printf("create customer thread error\n");
			return -1;
		}
	}
	
	for(i = 0; i < 4; i++){
		pthread_join(ptid[i], NULL);
		pthread_join(ctid[i], NULL);
	}
	return 0;
}

一次生成結果

productor push data:72535
productor push data:72476
productor push data:72536
productor push data:72477
productor push data:75147
customer pop data:72534
customer pop data:75146
customer pop data:75148
customer pop data:72532
customer pop data:72477

注意:

  1. 成員函數Push中的 lock 和 idle 順序不能換

如果我先加鎖了,然後去判斷有沒有空閒空間,若沒有就會阻塞;
但是這裏的阻塞與條件變量不一樣,不會解鎖
所以一定是先判斷,能夠訪問了才加鎖保護


如果本篇博文有幫助到您的理解,留個贊激勵博主呀~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章