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 顺序不能换

如果我先加锁了,然后去判断有没有空闲空间,若没有就会阻塞;
但是这里的阻塞与条件变量不一样,不会解锁
所以一定是先判断,能够访问了才加锁保护


如果本篇博文有帮助到您的理解,留个赞激励博主呀~~

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