【Linux】题解:生产者与消费者模型(附源代码)

【Linux】题解:生产者与消费者模型(附源代码)

摘要:本文主要介绍生产者与消费者模型,其中主要内容分为对该模型的介绍及分析,阻塞队列实现该模型,并对其升级实现多生产者多消费者并行执行。其中使用了信号量等方法,可以参考文章线程的同步与互斥


一、概述

所谓生产者与消费者模型,不可简单的理解为生产者生产后消费者对其进行消费的一对一模型,在学习线程后,与过往的函数调用比较,函数调用时把数据传入另一个函数中的参数中,在另一个函数中运行,在将其返回。该过程是一个串行的过程,可是在学习线程后则是可以通过一个临界区域完成数据的传递,并达成生产消费模型。其提高效率的本质就是将生产环节和消费环境进行解耦

**生产者消费者模式通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过临界资源来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给临界资源临界资源,消费者不找生产者要数据,而是直接从临界资源里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个临界资源就是用来给生产者和消费者解耦的。**示意图如下:

以生活中常见的超市作为例子。所谓生产者是对应的货品供货商,而临界资源则是超市,我们即是消费者。供应商供应货品到超市中,不用等待消费者来消费才会对商品处理,消费者只需要在超市中进行获取即可。其中超市平衡了生产者与供货商的处理能力,完成了顾客与供货商直接的解耦操作,可以并行的执行。

二、模型分析

对于关系分析,可以得出关于各个对象之间的关系,对于各个供应商而言,每个供应商共同需要访问的临界资源,因此对应的就是竞争互斥关系,而各个消费者亦然。对于生产者和消费者直接则是存在顺序问题,生产者生产后才可以给消费者供应,当然同时也需要访问临界资源,因此为同步互斥关系。关系总结如下:

  • 各个生产者:竞争互斥关系
  • 各个消费者:竞争互斥关系
  • 生产者和消费者:同步互斥关系

而对于执行流的分析,则是存在两个身份,分别为生产者与消费者,因此在创建线程时,创建的只有两种执行流。对于临界资源,可以为内存空间,可以为stl容器等。

三、基于阻塞队列的实现

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出,示意图如下:

3.1程序基本框架

// BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
namespace ns_blockqueue{
    template <class T>
    class BlockQueue{
    private:
        std::queue<T> blockQueue_;  // 阻塞队列
        int cap;    // 容量
    public:
        BlockQueue(){}
        ~BlockQueue(){}
    public:
        void Push(){
            // 生产
        }
        void Pop(){
            // 消费
        }
    };
}
// consumer_productor.cc
#include "BlockQueue.hpp"

using namespace ns_blockqueue;

// 消费者业务
void* consumerRun(void* args){
    BlockQueue<int> *blockqueue = (BlockQueue<int>*)args;
    while(true){
        blockqueue->Pop();
    }
}
// 生产者业务
void* productorRun(void* args){
    BlockQueue<int> *blockqueue = (BlockQueue<int>*)args;
    while(true){
        blockqueue->Push();
    }
}

int main(){
    // 阻塞队列
    BlockQueue<int> *blockqueue = new BlockQueue<int>();
    // 线程的创建与销毁:两个执行流——生产者与消费者
    pthread_t consumer,productor;
    pthread_create(&consumer,nullptr,consumerRun,(void*)blockqueue);
    pthread_create(&productor,nullptr,productorRun,(void*)blockqueue);

    pthread_join(consumer,nullptr);
    pthread_join(productor,nullptr);
    return 0;
}

3.2 临界资源保护与条件变量设置

在模型分析的过程中,需要对临界资源进行保护以及对生产者与消费者进行同步设置。对于临界资源的保护,通过互斥锁设置即可。而条件变量来完成同步的设置,首先只有生产才可以知道消费者什么时候进行消费,消费者知道,生产者什么时候可以进行生产。在临界资源生产满后,不再生产了让消费者消费,当临界资源消费空了,就不能够消费了,而是需要生产者生产。而生产者在生产后可以唤醒消费者消费,同理消费者消费后可以唤醒生产者进行生产。

为此定义一个互斥锁以及两个条件变量完成对线程的同步,以下代码包括了对各个变量的初始化与销毁以及对上述逻辑的代码编写,代码示例如下:

// BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
namespace ns_blockqueue{

    const int default_capacity = 10;

    template <class T>
    class BlockQueue{
    private:
        std::queue<T> blockQueue_;  // 阻塞队列
        int cap_;    // 容量
        pthread_mutex_t mtx_;    // 临界资源保护:互斥量
        pthread_cond_t is_full_;    // 阻塞队列满,生产者进行等待
        pthread_cond_t is_empty_;   // 阻塞队列空,消费者进行等待
    private:
        bool IsFull(){
            return blockQueue_.size() == cap_;
        }
        bool IsEmpty(){
            return blockQueue_.size() == 0;
        }
        void LockQueue(){
            pthread_mutex_lock(&mtx_);
        }
        void UnLockQueue(){
            pthread_mutex_unlock(&mtx_);
        }
        void ProductorWait(){
            pthread_cond_wait(&is_full_,&mtx_);
        }
        void ConsumerWait(){
            pthread_cond_wait(&is_empty_,&mtx_);
        }
        void WakeupConsumer(){
            pthread_cond_signal(&is_empty_);
        }
        void WakeupProductor(){
            pthread_cond_signal(&is_full_);
        }
    public:
        // 互斥量与条件变量初始化与销毁
        BlockQueue(int cap_ = default_capacity):cap_(cap_){
            pthread_mutex_init(&mtx_,nullptr);
            pthread_cond_init(&is_empty_,nullptr);
            pthread_cond_init(&is_full_,nullptr);
        }
        ~BlockQueue(){
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&is_empty_);
            pthread_cond_destroy(&is_full_);
        }
    public:
        void Push(const T &in){
            // 生产
            LockQueue();
            while(IsFull()){
                ProductorWait();
            }
            blockQueue_.push(in);
            UnLockQueue();
            WakeupConsumer();
        }
        void Pop(T *out){
            // 消费
            LockQueue();
            while(IsEmpty()){
                ConsumerWait();
            }
            *out = blockQueue_.front();
            blockQueue_.pop();
            UnLockQueue();
            WakeupProductor();
        }
    };
}

注意:此处在判断空与满时,是通过while循环判断的,这是为了防止存在挂起失败或者伪唤醒的情况。if判断不完善的原因在于,当出现上述问题时,实际上阻塞队列可能仍然处于满或者空的状态,此时会使得程序崩溃或者越界访问。

3.3 业务完成

随机数传输:通过产生随机数后,生产者将其产生于临界资源中,消费者从临界资源中进行消费,获取随机数。代码如下:

// consumer_productor.cc
#include "BlockQueue.hpp"
#include <cstdlib>
#include <time.h>
#include <unistd.h>
using namespace ns_blockqueue;
using namespace std;

// 消费者业务
void* consumerRun(void* args){
    BlockQueue<int> *blockqueue = (BlockQueue<int>*)args;
    while(true){
        sleep(2);
        int data = 0;
        blockqueue->Pop(&data);
        cout << "Consumer ID: " << pthread_self() << " consume number: " << data << endl;
    }
}
// 生产者业务
void* productorRun(void* args){
    BlockQueue<int> *blockqueue = (BlockQueue<int>*)args;
    while(true){
        int data = rand()%10 + 1;
        cout << "Productor ID: " << pthread_self() << " product number: "<< data << endl;
        blockqueue->Push(data);
        sleep(1);
    }
}

int main(){
    // 随机数产生
    srand((long long)time(nullptr));
    // 阻塞队列
    BlockQueue<int> *blockqueue = new BlockQueue<int>();
    // 线程的创建与销毁:两个执行流——生产者与消费者
    pthread_t consumer,productor;
    pthread_create(&consumer,nullptr,consumerRun,(void*)blockqueue);
    pthread_create(&productor,nullptr,productorRun,(void*)blockqueue);

    pthread_join(consumer,nullptr);
    pthread_join(productor,nullptr);
    return 0;
}
[root@VM-12-7-centos consumer_productor]# ./consumer_productor 
Productor ID: 140250556012288 product number: 1
Productor ID: 140250556012288 product number: 6
Consumer ID: 140250564404992 consume number: 1
Productor ID: 140250556012288 product number: 4
Productor ID: 140250556012288 product number: 9

任务派发与实现:生产者消费者模型不单单只是数据的传输,还有数据的处理过程,在此对其进行该场景进行实现。该实例是生产者派送关于随机数计算任务到临界区中,消费者获取任务并进行打印结构。代码如下:

// Task.hpp
#pragma once

#include <iostream>
#include <iomanip>
namespace ns_task{
    class Task{
    public:
        int x_;
        int y_;
        char op_;    // + - * / %
    public:
        Task(){
        }
        Task(int x,int y,char op){
            x_ = x;
            y_ = y;
            op_ = op;
        }
        ~Task(){}
        int Run(){
            int res = 0;
            switch (op_)
            {
            case '+':
                res = x_ + y_;
                break;
            case '-':
                res = x_ - y_;
                break;
            case '*':
                res = x_ * y_;
                break;
            case '/':
                res = x_ / y_;
            break;
            case '%':
                 res = x_ % y_;
                break;  
            default:
                std::cout <<"Task Error\n";
                return -1;
                break;
            }
            std::cout << std::left << std::setw(2) << "Task ---> "<< x_ << " " << op_ << " " << y_ << " = " << res <<std::endl;
            return 0;
        }
        int operator()(){
            return Run();
        }
    };
}
// consumer_productor.cc
#include "BlockQueue.hpp"
#include <cstdlib>
#include <time.h>
#include <unistd.h>
#include "Task.hpp"
#include <string.h>
#include <iomanip>

using namespace ns_blockqueue;
using namespace std;
using namespace ns_task;

// 消费者业务
void* consumerRun(void* args){
    BlockQueue<Task> *blockqueue = (BlockQueue<Task>*)args;
    while(true){
        sleep(2);
        Task task;
        blockqueue->Pop(&task);
        cout << std::left << setw(2)  << "Consumer ID: " << pthread_self();
        task();
    }
}
// 生产者业务
void* productorRun(void* args){
    BlockQueue<Task> *blockqueue = (BlockQueue<Task>*)args;
    std::string ops = "+-*/%";
    while(true){
        int x = rand()%20 + 1;
        int y = rand()%20 + 1;
        char op = ops[rand()%5];
        cout << std::left << setw(2) << "Productor ID: " << pthread_self() << " product task: "<< x << " " << op << " " << y << endl;
        Task task(x,y,op);
        blockqueue->Push(task);
        sleep(1);
    }
}

int main(){
    // 随机数产生
    srand((long long)time(nullptr));
    // 阻塞队列
    BlockQueue<Task> *blockqueue = new BlockQueue<Task>();
    // 线程的创建与销毁:两个执行流——生产者与消费者
    pthread_t consumer1,productor;
    pthread_t consumer2;
    pthread_t consumer3;
    pthread_t consumer4;
    pthread_create(&consumer1,nullptr,consumerRun,(void*)blockqueue);
    pthread_create(&consumer2,nullptr,consumerRun,(void*)blockqueue);
    pthread_create(&consumer3,nullptr,consumerRun,(void*)blockqueue);
    pthread_create(&consumer4,nullptr,consumerRun,(void*)blockqueue);

    pthread_create(&productor,nullptr,productorRun,(void*)blockqueue);

    pthread_join(consumer1,nullptr);
    pthread_join(consumer2,nullptr);
    pthread_join(consumer3,nullptr);
    pthread_join(consumer4,nullptr);
    pthread_join(productor,nullptr);
    return 0;
}
[root@VM-12-7-centos consumer_productor]# ./CPTask 
Productor ID: 140038218860288 product task: 19 + 16
Productor ID: 140038218860288 product task: 4 + 11
Consumer ID: 140038235645696 Working Task---> 19 + 16 = 35
Consumer ID: 140038227252992 Working Task---> 4 + 11 = 15
Productor ID: 140038218860288 product task: 6 * 20
Consumer ID: 140038252431104 Working Task---> 6 * 20 = 120

四、基于环形队列的实现

4.1 环形队列

在实现前先认识一下需要使用的数据结构——环形队列,环形队列是一种队列的变形,是固定大小的队列,在逻辑方面呈环状结构。一般实现方式有两种,分别为计数器来统计队列中的分布状况,也可以使用镂空一个位置来分辨队列的分布情况,此处主要介绍第一种。通过计数器与队列容量的大小比较来获知队列分布状态。

在此使用该数据结构,来完成对多线程情况下的并发访问临界资源,实现一个基于环形队列的生产者消费者模型。

4.2 资源保护与变量设置

在该模型中是存在互斥和同步特性的,在生产者和消费者开始时,指向同一位置表示队列为空的时候,应该让生产者进行生产,在生产者和消费者队列为满时,也是处于同一位置,此时需要让消费者先访问。当队列不为空或满时,生产者和消费者一定指向不同的位置,此时就是生产者和消费者可以并发执行。并且对于生产者或者消费者而言,不能够对同一身份的对象进行同一区域的访问,为此说明了该模型下的互斥特性。

可以得出结论,设计为生产者关心的资源是空余位置,而消费者关心的资源是队列中的数据,为此可以使用信号量对其进行同步互斥的设定。还需要注意的是,在多生产和多消费的情况下,使用信号量完成对同步和互斥的设定是对于整块临界资源的保护,但是由于在某块区域的写入读取过程中,可能无法保护,在此设定了互斥锁进行保护。

总结如下:

  • 信号量sem_blank:表示队列中空闲临界资源的大小,在生产者向队列写入数据后为生成空闲空间,进行P操作,在信号量不足时,表示队列满,生产者需要进行等待,在消费者消费数据后进行信号量释放,为V操作
  • 信号量sem_data:表示队列中临界资源的大小,在生产者向队列写入数据后进行V操作,在信号量不足时,表示队列为空,消费者需要进行等待,在消费者消费数据时进行信号量申请,为P操作
  • 互斥锁mutex_consumer:消费者间的竞争关系
  • 互斥锁mutex_productor:生产者间的竞争关系

4.3 环形队列实现

以下代码为唤醒队列类的构建,具体注意内容如下:

  • 通过索引来确定生产消费的位置,由于是环形队列,因此索引需要注意更新,防止越界等问题,方法为取模运算
  • 信号量和互斥锁的运用主要在于生产消费接口中,内容即是对分析内容的实现
  • 信号量与互斥锁是存在顺序关系的,主要原因是,互斥锁如果在信号量前加锁,程序不会出现问题,但是是串行执行的方式运行程序,多线程显得无意义。如果在之后加锁,说明为先申请信号量,在对于局部的临界资源进行互斥访问,可以达到并行的目的。
  • 在构造和析构函数中可以完成对应的变量初始化
// ringQueue.hpp
#pragma once

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

namespace ns_ringQueue
{
    // 默认容量
    const int default_capacity = 10;

    template<class T>
    class RingQueue{
    private:
        std::vector<T> ring_queue_;     // 队列结构
        int capacity_;  // 容量
        // 信号量与互斥锁
        pthread_mutex_t mutex_consumer_;
        pthread_mutex_t mutex_productor_;
        sem_t sem_blank_;
        sem_t sem_data_;
        // 生产者与消费者的索引
        int consumer_index_;
        int productor_index_;
    public:
        RingQueue(int capacity_ = default_capacity):ring_queue_(capacity_),capacity_(capacity_){
            // 数据初始化
            sem_init(&sem_blank_,0,capacity_);
            sem_init(&sem_data_,0,0);
            pthread_mutex_init(&mutex_consumer_,nullptr);
            pthread_mutex_init(&mutex_productor_,nullptr);
            consumer_index_ = 0;
            productor_index_ = 0;
        }
        ~RingQueue(){
            // 销毁
            sem_destroy(&sem_blank_);
            sem_destroy(&sem_data_);
            pthread_mutex_destroy(&mutex_consumer_);
            pthread_mutex_destroy(&mutex_productor_);
        }
    public:
        void Push(const T &in){
            // 生产接口
            sem_wait(&sem_blank_);  //申请信号量

            pthread_mutex_lock(&mutex_productor_);  //上锁:局部临界资源
           
            ring_queue_[productor_index_] = in;     //写入
            productor_index_++;     // 索引更新
            productor_index_ %= capacity_;

            pthread_mutex_unlock(&mutex_productor_);    //解锁
            
            sem_post(&sem_data_);   // 释放信号量
        }
        void Pop(T *out){
            // 消费接口
            sem_wait(&sem_data_);   //申请信号量
            
            pthread_mutex_lock(&mutex_consumer_);   //上锁:局部临界资源
            
            *out = ring_queue_[consumer_index_];    //输出
            consumer_index_++;
            consumer_index_ %= capacity_;   //索引更新
            
            pthread_mutex_unlock(&mutex_consumer_);     //解锁
            
            sem_post(&sem_blank_);      // 释放信号量
        }
    };
}

4.4 业务代码实现

业务实现:对于多生产者生产任务到循环队列中,对于多消费消费任务于信号队列中,并打印提示信息。该部分内容哦与阻塞队列的业务实现第二个实例相似,在此不做过多介绍,代码实例如下:

// ringConsumerProductor.cc
#include "ringQueue.hpp"
#include <unistd.h>
#include <pthread.h>
#include "Task.hpp"
using namespace ns_ringQueue;
using namespace std;
using namespace ns_task;
void* consumerRountine(void *args){
    RingQueue<Task> *rq = (RingQueue<Task> *)args;
    while(true){
        Task task;
        rq->Pop(&task);
        task();
        sleep(2);
    }
}
void* productorRountine(void *args){
    RingQueue<Task> *rq = (RingQueue<Task> *)args;
    std::string ops = "+-*/%";
    while(true){
        int x = rand()%20 + 1;
        int y = rand()%20 + 1;
        char op = ops[rand()%5];
        cout << std::left << setw(2) << "Productor ID: " << pthread_self() << " product task: "<< x << " " << op << " " << y << endl;
        Task task(x,y,op);
        rq->Push(task);
        sleep(1);
    }
}
int main(){
    RingQueue<Task> *rq = new RingQueue<Task>();

    pthread_t consumer1;
    pthread_t consumer2;
    pthread_t consumer3;
    pthread_t consumer4;

    pthread_t productor1;
    pthread_t productor2;
    pthread_t productor3;

    pthread_create(&consumer1,nullptr,consumerRountine,(void *)rq);
    pthread_create(&consumer2,nullptr,consumerRountine,(void *)rq);
    pthread_create(&consumer3,nullptr,consumerRountine,(void *)rq);
    pthread_create(&consumer4,nullptr,consumerRountine,(void *)rq);

    pthread_create(&productor1,nullptr,productorRountine,(void *)rq);
    pthread_create(&productor2,nullptr,productorRountine,(void *)rq);
    pthread_create(&productor3,nullptr,productorRountine,(void *)rq);

    pthread_join(consumer1,nullptr);
    pthread_join(consumer2,nullptr);
    pthread_join(consumer3,nullptr);
    pthread_join(consumer4,nullptr);

    pthread_join(productor1,nullptr);
    pthread_join(productor2,nullptr);
    pthread_join(productor3,nullptr);
    return 0;
}
// Task.hpp
#pragma once

#include <iostream>
#include <iomanip>
namespace ns_task{
    class Task{
    public:
        int x_;
        int y_;
        char op_;    // + - * / %
    public:
        Task(){
        }
        Task(int x,int y,char op){
            x_ = x;
            y_ = y;
            op_ = op;
        }
        ~Task(){}
        int Run(){
            int res = 0;
            switch (op_)
            {
            case '+':
                res = x_ + y_;
                break;
            case '-':
                res = x_ - y_;
                break;
            case '*':
                res = x_ * y_;
                break;
            case '/':
                res = x_ / y_;
            break;
            case '%':
                 res = x_ % y_;
                break;  
            default:
                std::cout <<"Task Error\n";
                return -1;
                break;
            }
            std::cout << std::left << std::setw(2) << "Task Running ---> "<< x_ << " " << op_ << " " << y_ << " = " << res <<std::endl;
            return 0;
        }
        int operator()(){
            return Run();
        }
    };
}
[root@VM-12-7-centos Blog_sem]# ./ringConsumerProductor 
Productor ID: 139731206522624 product task: 4 * 7
Task Running ---> 4 * 7 = 28
Productor ID: 139731214915328 product task: 16 + 14
Task Running ---> 16 + 14 = 30
Productor ID: 139731198129920 product task: 7 % 13
Task Running ---> 7 % 13 = 7
Productor ID: 139731206522624 product task: 2 * 3
Productor ID: 139731214915328 product task: 11 / 20

补充:

  1. 代码将会放到:Linux_Review: Linux博客与代码 (gitee.com) ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!
  • 6
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fat one

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值