【Linux】生产者消费者模型

什么是生产者消费者模型?

生产者消费者模型是操作系统中一种重要的模型,它描述的是一种等待和通知的机制

一. 引子

生活中就有很多生产者消费者模型,比如购物
我们会取超市买一些生活用品,显而易见,我们就是消费者。那超市是生产者吗?其实不是,超市只是一个交易场所,真正的生产者是工厂,也就是供应商。
而将这种关系应用到多线程中,生产者就是承担生产数据的线程消费者就是接收数据的线程,而交易场所是一种特殊的缓冲区

在这里插入图片描述

而整个过程也可以类比成送快递

1.商家将商品包装好 —— 相当于生产者生产数据
2.商家将快递放到快递站 —— 相当于生产者将数据放入缓冲区
3.快递员将商品从快递站取出 —— 相当于消费者将数据从缓冲区取出
4.快递员将商品送出 —— 相当于消费者对数据做处理

二. 生产者消费者模型的作用

其实,生产者消费者模型通过交易场所,也就是缓冲区,完成了生产者和消费者之间的解耦,生产者和消费者没有直接交互,统一通过和缓冲区交互,来达到数据传递的效果。
同时,缓冲区还解决了,多个消费者,生产者要将数据传递给谁;多个生产者,消费者要从谁那拿数据。但生产和消费不同步时,缓冲区也可以完成任务。生产者多生产的数据,存放到缓冲区中,方便在不生产时,消费者仍有数据可读;再消费者不读取数据时,缓冲区满时,提醒生产者不需要再生产了。

这就是生产者消费者模型的好处。

三. 生产者/消费者模型的记忆原则

生产者消费者模型中有三种关系:

生产者—生产者
消费者—消费者
生产者—消费者

而三者之间的关系如下:

  1. 生产者—生产者:互斥
    因为缓冲区大小有限,所以放数据时,生产者之间需要互斥
  2. 消费者—消费者:互斥
    对于同一种资源(数据),消费者之间存在竞争关系,所以需要互斥
  3. 生产者—消费者:既同步,又互斥
    在生产者放入数据和消费者取出数据时,都不允许对方对缓冲区进行操作,避免受到影响,所以需要互斥。另外缓冲区没有数据时,消费者不能读数据;缓冲区数据满时,生产者不能放数据,所以生产者和消费者需要按照一定顺序访问缓冲区,所以二者之间还是同步

四. 简单的生产者消费者模型

生产者消费者模型就两个关系生产者和消费者
还有一个场景交易场所(缓冲区)
而通过交易场所的不同,我们可以实现 不同的生产者消费者模型

1. 阻塞队列

阻塞队列是实现生产者消费者模型的一种缓冲区

BlockQueue,阻塞队列,是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取数据的操作将被阻塞,直到队列被放入数据;当队列满时,往队列里存放数据的操作也会被阻塞,直到有元素被从队列中取出(以上操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

在这里插入图片描述
图中Thread 1是生产者,Thread 2是消费者。

接下来,我们简单使用阻塞队列实现生产者消费者模型
条件变量相关使用可以参看【Linux】线程同步&条件变量
互斥量相关使用可以参看【Linux】线程互斥
程序的目的是:生产者通过随机数种子生成数字,再通过阻塞队列,传送给消费者。消费者通过阻塞队列获取数据,然后输出。


首先,我们先实现阻塞队列
因为生产者和消费者之间是同步并互斥的关系,所以在生产者放数据,消费者取数据时,都应该保持互斥,并且根据阻塞队列的容量大小,按一定顺序执行动作

为此,我们需要在阻塞队列中同时封装互斥锁和条件变量

#include<iostream>
#include<queue>
#include<pthread.h>

//默认的队列的大小
int gcap=5;

template<class T>//模板
class blockQueue
{
private:
    std::queue<T> _q;//存放资源的队列
    int _cap;//队列的容量
    pthread_mutex_t _mutex;//互斥锁
    pthread_cond_t _consumerCond;//消费者的条件变量,当队列空时,wait
    pthread_cond_t _producerCond;//生产者的条件变量,当队列满时,wait
};

因为生产者和消费者等待的条件不一样,所以需要两个条件变量,但是二者访问同一临界区,所以只需要一个互斥锁

在阻塞队列初始化时,我们需要对互斥锁和条件变量做初始化,在阻塞队列销毁时,也同样需要对其进行销毁。

	//构造函数
    blockQueue(int cap=gcap)
    :_cap(cap)
    {
        //初始化锁和条件变量
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_consumerCond,nullptr);
        pthread_cond_init(&_producerCond,nullptr);
    }
    //析构函数
    ~blockQueue()
    {
    	//销毁互斥锁和条件变量
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_producerCond);
    }

接下来是生产者放数据的函数

访问临界区前一定需要加锁,判断条件也是在访问临界资源,所以也需要在加锁后。
生产者需要先判断当前阻塞队列数据是否已满,如果满了,则需要在_producerCond条件变量上等待,而如果等待被唤醒,则会继续从wait处开始运行,放入数据,同时因为有数据产生,可以唤醒等待的消费者

	//判断队列是否满
    bool isFull(){  return _cap==_q.size();  }

	//放数据
    void push(T&in)
    {
        //访问临界区

        //加锁
        pthread_mutex_lock(&_mutex);

        //判断队列是否满
        while(isFull())
        {
            //队列为满说明不需要再生产,开始等待
            pthread_cond_wait(&_producerCond,&_mutex);
        }

        //放入数据
        _q.push(in);
        //有生产数据就可以唤醒消费者
        pthread_cond_signal(&_consumerCond);
        //解锁
        pthread_mutex_unlock(&_mutex);
    }

消费者取数据

	//判断队列是否为空
    bool isEmpty(){  return _q.empty();  }

	//取数据
    void pop(T*out)
    {
        //访问临界区加锁
        pthread_mutex_lock(&_mutex);

        //判断队列是否为空
        while(isEmpty())
        {
            //为空则不需要再拿数据
            //在消费者条件变量上等待
            pthread_cond_wait(&_consumerCond,&_mutex);
        }
        //取出数据
        *out=_q.front();
        _q.pop();

        //唤醒生产者
        pthread_cond_signal(&_producerCond);
        //访问结束,解锁
        pthread_mutex_unlock(&_mutex);
    }

消费者取数据的基本思路同生产者放数据,不过消费者需要判断的条件是当前阻塞队列是否为空,同时out作为输出型参数,需要返回取出的数据,然后因为取出数据,有空位可以生产,所以唤醒等待的生产者。

以上就是简单的阻塞队列的实现方式,完整代码如下:

blockQueue.hpp

#include<iostream>
#include<queue>
#include<pthread.h>

//默认的队列的大小
int gcap=5;

template<class T>
class blockQueue
{
public:
    //构造函数
    blockQueue(int cap=gcap)
    :_cap(cap)
    {
        //初始化锁和条件变量
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_consumerCond,nullptr);
        pthread_cond_init(&_producerCond,nullptr);
    }
    //析构函数
    ~blockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_producerCond);
    }

    //判断队列是否满
    bool isFull(){  return _cap==_q.size();  }

    //判断队列是否为空
    bool isEmpty(){  return _q.empty();  }

    //放数据
    void push(T&in)
    {
        //访问临界区

        //加锁
        pthread_mutex_lock(&_mutex);

        //判断队列是否满
        while(isFull())
        {
            //队列为满说明不需要再生产,开始等待
            pthread_cond_wait(&_producerCond,&_mutex);
        }

        //放入数据
        _q.push(in);
        //有生产数据就可以唤醒消费者
        pthread_cond_signal(&_consumerCond);
        //解锁
        pthread_mutex_unlock(&_mutex);
    }

    //取数据
    void pop(T*out)
    {
        //访问临界区加锁
        pthread_mutex_lock(&_mutex);

        //判断队列是否为空
        while(isEmpty())
        {
            //为空则不需要再拿数据
            //在消费者条件变量上等待
            pthread_cond_wait(&_consumerCond,&_mutex);
        }
        //取出数据
        *out=_q.front();
        _q.pop();

        //唤醒生产者
        pthread_cond_signal(&_producerCond);
        pthread_mutex_unlock(&_mutex);

        //访问结束,解锁
        pthread_mutex_unlock(&_mutex);
    }

private:
    std::queue<T> _q;//存放资源的队列
    int _cap;//队列的容量
    pthread_mutex_t _mutex;//互斥锁
    pthread_cond_t _consumerCond;//消费者的条件变量,当队列空时,wait
    pthread_cond_t _producerCond;//生产者的条件变量,当队列满时,wait
};

简单使用一下

main.cc

#include "blockQueue.hpp"
#include <ctime>
#include <unistd.h>

using namespace std;

// 消费者生产者启动函数
void *consumer(void *args)
{
    blockQueue<int> *bq = static_cast<blockQueue<int> *>(args);
    int data=0;
    while(true)
    {
        sleep(1);
        bq->pop(&data);
        cout<<"消费者获取数据: "<<data<<endl;
    }
}

// 生产者启动函数
void *producer(void *args)
{
    blockQueue<int> *bq = static_cast<blockQueue<int> *>(args);
    while (true)
    {
        sleep(1);
        // 生产1~10的数字
        int val = rand() % 10 + 1;
        cout<<"生产者生产数据: "<<val<<endl;
        bq->push(val);
    }
}

int main()
{
    blockQueue<int> *bq=new blockQueue<int>();
    // 随机数种子
    srand((uint64_t)time(nullptr));

    pthread_t Consumer;
    pthread_t Producer;
    //创建线程
    pthread_create(&Consumer, nullptr, consumer, bq);
    pthread_create(&Producer, nullptr, producer, bq);
    //线程等待
    pthread_join(Consumer,nullptr);
    pthread_join(Producer,nullptr);
	
	delete dp;
	return 0;
}

运行结果如下:

在这里插入图片描述

2. 环形队列

环形队列采用数组模拟,用模运算来模拟环状特性。
使用信号量控制生产者和消费者可访问的空间数量

首先设计环形队列:环形队列就是交易场所。
不同于阻塞队列,生产者和消费者访问的大概率是不同的位置,将环形队列不看作整体,而是根据容量分散成每一部分,并使用两个信号量控制——空间信号量(生产者关心)和资源信号量(生产者关心)

基本框架如下:

#include<iostream>
#include<vector>
#include<semaphore.h>
#include<mutex>

//环形队列的默认大小
static const int N=5;

template<class T>
class RingQueue
{
public:
    RingQueue(int num=N):_ring(num),_cap(num)
    {
        //第一个0表示线程间共享,第二个0代表初始值为0
        sem_init(&_data_sem,0,0);
        sem_init(&_space_sem,0,num);

        //互斥锁初始化
        pthread_mutex_init(&_c_mutex,nullptr);
        pthread_mutex_init(&_p_mutex,nullptr);

        _c_pos=_p_pos=0;//环形队列访问位置初始化
    }
    
    ~RingQueue()
    {
    	//信号量的销毁
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
		//互斥锁的销毁
        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }

private:
    std::vector<T>_ring;//环形队列
    int _cap;//容量
    sem_t _data_sem;//资源信号量,消费者关心
    sem_t _space_sem;//空间信号量,生产者关心
    int _c_pos;//消费者访问位置
    int _p_pos;//生产者访问位置

    pthread_mutex_t _c_mutex;//消费者互斥锁
    pthread_mutex_t _p_mutex;//生产者互斥锁
}; 

然后是放资源拿资源的逻辑:

	//申请信号量
    void P(sem_t &s)
    {
        sem_wait(&s);
    }

    //归还信号量
    void V(sem_t &s)
    {
        sem_post(&s);
    }
	
	//生产者
    void push(const T&in)
    {
        P(_space_sem);//空间信号量-1
        pthread_mutex_lock(&_p_mutex);

        _ring[_p_pos++]=in;
        _p_pos%=_cap;

        pthread_mutex_unlock(&_p_mutex);
        V(_data_sem);//资源信号量+1
    }

    //消费者
    void pop(T&out)
    {
        P(_data_sem);//资源信号量-1
        pthread_mutex_lock(&_c_mutex);

        out=_ring[_c_pos++];//取出资源
        _c_pos%=_cap;

        pthread_mutex_unlock(&_c_mutex);
        V(_space_sem);//空间信号量+1
    }

完整代码如下:

#include<iostream>
#include<vector>
#include<semaphore.h>
#include<mutex>

//环形队列的默认大小
static const int N=5;

template<class T>
class RingQueue
{
private:
    //申请信号量
    void P(sem_t &s)
    {
        sem_wait(&s);
    }

    //归还信号量
    void V(sem_t &s)
    {
        sem_post(&s);
    }
public:
    RingQueue(int num=N):_ring(num),_cap(num)
    {
        //第一个0表示线程间共享,第二个0代表初始值为0
        sem_init(&_data_sem,0,0);
        sem_init(&_space_sem,0,num);

        //互斥锁初始化
        pthread_mutex_init(&_c_mutex,nullptr);
        pthread_mutex_init(&_p_mutex,nullptr);

        _c_pos=_p_pos=0;//环形队列访问位置初始化
    }
    //生产者
    void push(const T&in)
    {
        P(_space_sem);//空间信号量-1
        pthread_mutex_lock(&_p_mutex);

        _ring[_p_pos++]=in;
        _p_pos%=_cap;

        pthread_mutex_unlock(&_p_mutex);
        V(_data_sem);//资源信号量+1
    }

    //消费者
    void pop(T&out)
    {
        P(_data_sem);//资源信号量-1
        pthread_mutex_lock(&_c_mutex);

        out=_ring[_c_pos++];//取出资源
        _c_pos%=_cap;

        pthread_mutex_unlock(&_c_mutex);
        V(_space_sem);//空间信号量+1
    }

    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }

private:
    std::vector<T>_ring;//环形队列
    int _cap;//容量
    sem_t _data_sem;//资源信号量,消费者关心
    sem_t _space_sem;//空间信号量,生产者关心
    int _c_pos;//消费者访问位置
    int _p_pos;//生产者访问位置

    pthread_mutex_t _c_mutex;//消费者互斥锁
    pthread_mutex_t _p_mutex;//生产者互斥锁
}; 

结束语

感谢你的阅读

如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值