【Linux探索学习】第三十二弹——生产消费模型:基于阻塞队列和基于环形队列的两种主要的实现方法

article/2025/8/11 12:47:45

Linux学习笔记:

https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482

前言:

在前面我们已经学习了关于线程的主要知识,包括线程的基础知识以及线程的同步与互斥等内容,今天我们来学几个线程知识的拓展内容和几个经典的应用场景,比如:生产消费模型(cp问题)、线程池、有关线程的单例模式等,下面我们就来看一下这几点内容(本篇为生产消费模型,线程池和单例模式在后面讲)

目录

1. 为什么要使用生产消费模型

2. 生产消费模型优点

3. 基于阻塞队列的生产消费模型

3.1 简单实现

3.2 伪唤醒

3.3 拓展

4. 基于环形队列的生产消费模型

4.1 环形队列

4.2 代码实现

5. 总结


1. 为什么要使用生产消费模型

我们来看这样一个例子

在一个超市购货和售货系统中,超市相当于临界于供货商和消费者之间的资源,一个超市不止有一个供货商,当然也不止有一个消费者,这些供货商和消费者之间正是因为超市的存在所以可以避免互相接触,供货商只需要将商品直接批发给超市就好了,消费者去超市货架上获取商品,这样就起到解耦的作用,从而提高了效率

生产消费模型正是如此,不同的是超市我们用一些特定结构的内存空间来表示,比如阻塞循环队列等,而供货商和生产者我们则是用线程来替代

既然我们说超市是用特定结构的内存空间来代替的,而且它还处于生产者和消费者之间,所以它本质上也是作为临界资源存在的,既然是临界资源就一定存在一定的同步与互斥问题,具体的关系如下图所示:

我们可以把这三种关系结合上面讲的生产消费模型的结构总结为“321原则”

2. 生产消费模型优点

  1. 解耦生产与消费
    生产者和消费者通过共享的缓冲区(如队列)进行交互,彼此独立运行,降低了系统耦合度。

  2. 提高资源利用率
    生产者和消费者可以并行工作,减少等待时间,提升系统整体效率。

  3. 负载均衡
    缓冲区能够平衡生产者和消费者的处理速度差异,避免因速度不匹配导致的性能问题。

  4. 增强系统扩展性
    可以灵活增加生产者或消费者数量,适应不同负载需求,提升系统扩展能力。

  5. 异步处理
    生产者无需等待消费者处理完成即可继续生产,提高系统响应速度。

  6. 流量控制
    通过缓冲区大小限制,防止生产者过快生产导致系统过载,增强系统稳定性。

  7. 简化设计
    将复杂的流程分解为生产、消费和缓冲区三个部分,简化系统设计和维护。

  8. 支持并发
    多个生产者和消费者可以同时操作缓冲区,提升并发处理能力。

  9. 增强容错性
    生产者和消费者的故障不会直接影响对方,缓冲区还能在故障恢复后继续处理数据。

  10. 适应多种场景
    该模型广泛应用于消息队列、任务调度、数据流处理等领域,适用性强。

3. 基于阻塞队列的生产消费模型

3.1 简单实现

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

我们先来看一下我们基于阻塞队列建立的生产消费模型的基本格式:

#pragma once#include<iostream>
#include<queue>
#include<pthread.h>template<class T>
class BlockQueue
{static int defaultcap=1;
public:BlockQueue(int maxcap=defaultcap):max_cap(maxcap){pthread_mutex_init(&mutex_,nullptr);pthread_cond_init(&p_cond_,nullptr);pthread_cond_init(&c_cond_,nullptr);}void Push(const T& in){//生产}T Pop(){//消费}~BlockQueue(){pthread_cond_destroy(&p_cond_);pthread_cond_destroy(&c_cond_);pthread_mutex_destroy(&mutex_);}
private:queue<T> q_;  //共享资源int max_cap;  //最大容量pthread_mutex_t mutex_;   //只需要有一把锁就够了,在我们创建的这个简单的生产消费模型中不存在同时生产和消费的场景pthread_cond_t p_cond_;pthread_cond_t c_cond_;   //创建了两个条件变量,因为生产者和消费者启动的条件不一样
};

在这里我们把这个阻塞队列作为共享资源,生产和消费都是在这个内存空间中对数据进行操作的,所以我们需要一把锁确保这个共享资源不被多次占用

下面我们看一下生产和消费的具体方法:

生产(Push):

    void Push(const T& in){//生产者pthread_mutex_lock(&mutex_);if(q_.size()==max_cap){pthread_cond_wait(&p_cond_,&mutex_);  //当队列中没有数据时,激活条件变量同时把锁释放}q_.push(in);//因为我们创建的是一个简单的生产消费模型,且最大容量为1,所以当我们生产一个数据后就达到了最大容量,需要激活消费者pthread_cond_signal(&c_cond_);pthread_mutex_unlock(&mutex_);}

消费(Pop):

    T Pop(){//消费者pthread_mutex_lock(&mutex_);if(q_.size()==0){pthread_cond_wait(&c_cond_,&mutex_);}T out=q_.front();q_.pop();pthread_cond_signal(&p_cond_);pthread_mutex_unlock(&mutex_);return out;}

总体来说生产和消费的实现方法也是很简单的,抛开锁和信号量的使用来讲,就是很简单的往一个队列中插入和拿取数据,锁和信号量的使用方法在前面讲过,各位可以结合注释看一下如何用

以上就是我们基于阻塞队列的一个简单的生产消费模型,下面我们结合一段代码来使用一下我们的生产消费模型:

#include"BlockQueue.hpp"
#include<iostream>
#include<pthread.h>
#include<ctime>
#include<unistd.h>
using namespace std;void *Productor(void *args)
{//args是void*类型的,我们需要先转换BlockQueue<int> *bq=static_cast<BlockQueue<int>*>(args);//让我们的线程处于死循环的状态,即一直生产while(true){int in=rand()%10+1;usleep(10);   //模拟处理数据所需时间bq->Push(in);cout<<"Productor make a num: "<<in<<" threadid: "<<pthread_self()<<endl;sleep(1);  //让生产者在生产一个数据后睡眠一秒,便于观察打印结果}return nullptr;
}
void *Consumer(void *args)
{BlockQueue<int> *bq=static_cast<BlockQueue<int>*>(args);while(true){int out=bq->Pop();usleep(100);   //模仿数据处理所需时间cout<<"Consumer get a num: "<<out<<" threadid: "<<pthread_self()<<endl;}return nullptr;
}
int main()
{srand(time(nullptr));BlockQueue<int> *bq=new BlockQueue<int>();pthread_t c,p;//创建生产和消费两个线程,并且将bq作为参数传给两者pthread_create(&p,nullptr,Productor,bq);pthread_create(&c,nullptr,Consumer,bq);//回收线程pthread_join(c,nullptr);pthread_join(p,nullptr);delete bq;return 0;
}

执行结果(截取部分):

此时我们就可以看到我们的生产者创建一个数据,并将这个数据传给阻塞队列,也就是临界资源中,消费者从里面拿数据,同时也没有出现互斥等问题

3.2 伪唤醒

其实在生产者和消费者互相唤醒的过程中,我们有时候可能会出现这样的一种错误

比如如果我们此时有多个生产者,并且条件队列中有多个线程在等待,我们的消费者此时如果消费了一个数据,按理说就会唤醒等待队列最前面的线程获取锁资源并生产一个数据,但是如果我们在唤醒时采用的是全部唤醒的方式呢?那么此时等待队列中的线程都会被唤醒了,同时竞争锁,在第一个线程获得锁并将数据生产后,第二个线程仍会获得锁并生产数据,但是数据只消耗了一个,所以此时就会引发错误,我们把导致这种错误的原因叫做伪唤醒

那么如何解决伪唤醒的问题呢?

很简单,我们采用while循环的方式来进行判断,这样每一个线程在获取锁执行下一步时都要进行判断

3.3 拓展

上面我们讲的就是一个最简单的生产消费模型,但实际上生产消费模型可以在更多的场景下进行使用,比如在上面我们往循环队列中传的是整数,但是我们也可以创建任务传进去,这样消费者得到的就是各种任务

再比如我们上面创建的是单生产者和单消费者,但如果有多个执行者呢?我们该如何做呢?

很简单,我们直接创建就行,因为不管几个生产者和消费者,它们访问的都还是同样的资源,所以还是只需要一把锁就足够了,并不需要改动什么,最核心的原因是它仍然满足“321原则"
改成多生产者多消费者的意义就是可以提高效率,比如单消费者在获取数据后需要将数据处理后才能重新获取下一个数据并处理,但是多消费者后在一个消费者处理数据的同时,另一个消费者就可以去获取数据,从而提高效率

总之,生产消费模型的功能还是挺多的,以上就是生产消费模型的原理和核心内容,感兴趣的可以再把我上面提到的那个传任务的方式试一下

4. 基于环形队列的生产消费模型

4.1 环形队列

其实不管是以何种内存结构构件的生产消费模型,其核心都是一样的,这里我们主要是想结合POSIX信号量来创建一个生产消费模型,同时带大家再多看一个数据结构类型——环形队列搭建的生产消费模型

我们先来看一下环形队列是什么

学习过数据结构的应该对环形队列都有所了解,环形队列实际上就是队列首位相连的一种特殊形式,它最关键的地方就在于,我们如何判断其空满状态,我们是通过判断首位位置来做到的

在本处我们结合POSIX信号量来创建生产消费模型的过程中,实际上我们可以不用自己去判断队列中是否已满或为空,我们可以通过信号量来处理,因为我们之前讲过,信号量的本质就是一个计数器,它能够帮助我们统计我们所关注的资源的个数,在这里,生产者关注的就是还有多少个空位,我们称作空间资源,消费者关注的是有多少数据,我们称为数据资源

因为我们不需要去讨论队列为空或为满,所以其实我们可以用vector数组来替代环形队列

4.2 代码实现

代码实现与上面的阻塞队列的方法没啥大的区别,该注意的地方我在代码中注释出来了,这里就直接上代码了

Sem.hpp

#pragma once
#include<semaphore.h>class Sem
{
public:Sem(int value){sem_init(&sem_,0,value);}void P(){sem_wait(&sem_);}void V(){sem_post(&sem_);}~Sem(){sem_destroy(&sem_);}
private:sem_t sem_;
};

RingQueue.hpp

#pragma once#include"sem.hpp"
#include<iostream>
#include<pthread.h>
#include<vector>
using namespace std;static int defaultcap=5;
template<class T>
class RingQueue
{
public:RingQueue(int mp=defaultcap):max_cap(mp),rq(mp),p_index(0),c_index(0),p_sem(mp),c_sem(0){pthread_mutex_init(&p_mutex,nullptr);pthread_mutex_init(&c_mutex,nullptr);}void Push(T& in){p_sem.P();pthread_mutex_lock(&p_mutex);rq[p_index++]=in;p_index%=max_cap;pthread_mutex_unlock(&p_mutex);c_sem.V();}T Pop(){c_sem.P();pthread_mutex_lock(&c_mutex);T out=rq[c_index++];c_index%=max_cap;pthread_mutex_unlock(&c_mutex);p_sem.V();return out;}~RingQueue(){pthread_mutex_destroy(&p_mutex);pthread_mutex_destroy(&c_mutex);}
private:vector<T> rq;       //用vector模拟环形队列int max_cap;        //队列的容量int p_index;        //生产者下标int c_index;        //消费者下标Sem p_sem;          //空间信号量(生产者所关注的信号量)Sem c_sem;          //数据信号量(消费者所关注的信号量)pthread_mutex_t p_mutex;pthread_mutex_t c_mutex;
};

Main.cc

#include"RingQueue.hpp"
#include<iostream>
#include<pthread.h>
#include<ctime>
#include<unistd.h>
using namespace std;void *Productor(void *args)
{//args是void*类型的,我们需要先转换RingQueue<int> *rq=static_cast<RingQueue<int>*>(args);//让我们的线程处于死循环的状态,即一直生产while(true){int in=rand()%10+1;usleep(10);   //模拟处理数据所需时间rq->Push(in);cout<<"Productor make a num: "<<in<<" threadid: "<<pthread_self()<<endl;sleep(1);  //让生产者在生产一个数据后睡眠一秒,便于观察打印结果}return nullptr;
}
void *Consumer(void *args)
{RingQueue<int> *rq=static_cast<RingQueue<int>*>(args);while(true){int out=rq->Pop();usleep(100);   //模仿数据处理所需时间cout<<"Consumer get a num: "<<out<<" threadid: "<<pthread_self()<<endl;}return nullptr;
}
int main()
{srand(time(nullptr));RingQueue<int> *rq=new RingQueue<int>();pthread_t c,p;//创建生产和消费两个线程,并且将bq作为参数传给两者pthread_create(&p,nullptr,Productor,rq);pthread_create(&c,nullptr,Consumer,rq);//回收线程pthread_join(c,nullptr);pthread_join(p,nullptr);delete rq;return 0;
}

运行结果:

5. 总结

生产消费模型在外面平时的日常生活中还是比较常见的,它能够起到很好的解耦作用,提高系统整体效率和相应速度,同时,也能帮助我们更好的理解线程互斥与同步的问题,更加熟练的使用互斥锁、条件变量、POSIX信号量等,总之,学习完本篇生产消费模型,相信会对你对线程知识的掌握和如何提高系统整体效率的能力有更好的理解

本篇笔记:

感谢各位大佬观看,创作不易,还望各位大佬点赞支持!!!


http://www.hkcw.cn/article/akySfeZJJp.shtml

相关文章

复现FAST_LIVO2【Ubuntu 20.04.6 LTS】

目录 准备1 Ubuntu 和 ROS2 其他库2.1 PCL2.2 Eigen2.3 OpenCV 3 Sophus3.1 安装3.2 make报错 4 Vikit5 livox_ros_driver5.1 Livox-SDK5.2 livox_ros_driver FAST-LIVO2运行参考 准备 1 Ubuntu 和 ROS 依据开源介绍&#xff0c;Ubuntu 16.04~20.04。 复现版本为&#xff1a;…

【基于Ubuntu下Yolov5的目标识别】保姆级教程 | 虚拟机安装 - Ubuntu安装 - 环境配置(Anaconda/Pytorch/Vscode/Yolov5) |全过程图文by.Akaxi

目录 一.【YOLOV5算法原理】 1.输入端 2.Backbone 3.Neck 4.输出端 二&#xff0e;【系统环境】 1.虚拟机的安装与创建 2.安装Ubuntu操作系统 3.环境的配置 3.1.Ubuntu下Anacoda安装以及虚拟环境配置 3.2.Pytorch安装 3.3.Vscode安装 3.4.Yolov5源码及环境获取安装…

[已解决] 本地两台 win电脑 (以太网) 网线传输文件 - 局域网连接 (解决windows无法访问共享文件问题 - Windows 安全中心输入网络凭据 用户名/密码 不正确问题)

背景 由于要本地传输的数据比较大&#xff0c;大几百GB网盘传输慢&#xff0c;正好有网线&#xff0c;试着本地网线高速传输&#xff08;实测113MB/s&#xff09;踩了很多坑&#xff0c;想把亲测成功的经验分享出来帮助更多同学 目录 1 网线接入 2 设置两台电脑的IP地址 3 …

Docker(三):DockerFile

一、DockerFile介绍 1、DockerFile 介绍 DockerFile 是一种能够被Docker 程序解释的文件&#xff08;一般为了方便理解称之为“剧本”&#xff09;。 DockerFile 由一条一条的指令组成&#xff0c;并且有自己的书写格式和支持的命令。当我们需要在容器 镜像中指定自己额外的需…

【Linux网络编程】第十弹---打造初级网络计算器:从协议设计到服务实现

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】【Linux网络编程】 目录 1、Protocol.hpp 1.1、Request类 1.1.1、基本结构 1.1.2、构造析构函数 1.1.3、序列化函数 1.1.4、反…

Ubuntu24安装Docker详细教程

目录 Ubuntu 安装 Docker 详细教程 一、安装环境说明 二、卸载旧版 Docker&#xff08;若存在&#xff09; 三、安装必要的依赖 四、添加 Docker 的 GPG 密钥 五、配置 Docker 的软件源 六、安装 Docker docker-compose离线安装 七、验证 Docker 是否安装成功 八、配置…

2024第八届御网杯信息安全网络大赛线上WP详解(misc+cryoto)(详解-思路-脚本)

芜湖~ 首届御网杯线上和ISCC分开进行 但还是用的ISCC的页面差评 嘻嘻 又是玄乎的一天 以下是我自己的一些思路和解析 有什么问题或者建议随时都可以联系我 目录 附件 # Misc ##Notice ##编码转换 Brainfuck编码 jsfuck编码 Ook! 编码 ##bluetooth 导出压缩包 第一…

在Linux中安装、配置和挂载NFS的完整指南

一、NFS简介 NFS&#xff08;Network File System&#xff09; 是一种分布式文件系统协议&#xff0c;允许用户通过网络在不同主机间共享文件和目录。它适用于局域网环境&#xff0c;常用于服务器集群、数据共享等场景。本文详细介绍NFS服务端与客户端的安装、配置及挂载流程。…

Linux-Ubuntu下的git安装与配置

一、安装git 1.打开终端&#xff0c;运行以下命令&#xff08;需要联网&#xff09; sudo apt-get update sudo apt-get install git 2.验证安装 安装完成之后&#xff0c;通过运行以下命令验证git是否已经正确安装&#xff1a; git --version 二、配置git 2.1.配置用户名…

亲测可用:wsl2安装ubuntu22.04的GNOME桌面

本文主要介绍wsl安装的ubuntu如何配置图形化桌面&#xff0c;主要使用与windows操作系统环境&#xff0c;方便搭建Linux环境下的可视化开发环境&#xff0c;网上流传的很多教程都不能正确安装&#xff0c;以下是本人亲自验证可用的操作方法。 1、开始安装 1.1 配置源 sudo v…

个人健康中枢的多元化AI网络革新与精准健康路径探析

引言 随着数字化转型的深入推进,个人健康中枢作为集成化健康管理系统,正在从传统的单一功能向多元化的AI驱动方向快速发展。在这一背景下,新兴网络硬件技术,特别是DPU(数据处理单元)和全光网络的出现,为个人健康中枢的革新提供了前所未有的机遇。本研究将深入探讨这些技…

Linux《进程控制》

在之前的Linux《进程概念》当中我们已经了解了进程基本的概念&#xff0c;那么接下来在本篇当中我们将开始进程控制的学习&#xff1b;在本篇当中我们先会对之前的学习的创建子进程的系统调用fork再进行补充了解&#xff0c;并且再之后会重点的学习进程的终止、进程等待以及进程…

Java应用中 慢SQL导致内存无法回收,然后导致线程阻塞,CPU被撑爆

问题分析 慢SQL的直接危害 数据库连接池长时间被占用&#xff0c;导致线程堆积&#xff0c;请求阻塞。 未释放的 ResultSet、Statement 或 Connection 可能导致内存泄漏&#xff08;例如未正确关闭资源&#xff09;。 大结果集&#xff08;如一次性加载百万条数据到内存&…

高质量AI歌曲生成器ACE-Step一键启动整合包,AI自动谱曲自动演唱

本次分享一款AI歌曲创作利器&#xff1a;ACE-Step&#xff0c;ACE-Step是刚发布不久的AI自动谱曲AI自动演唱软件&#xff0c;软件在歌曲生成速度、音乐连贯性和可控性上相对同类软件有了较大提升。ACE-Step在3小时前刚发布了新版本&#xff0c;我基于当前最新版本制作了免安装一…

Facebook 的隐私保护措施是否足够?技术观点

在数字时代&#xff0c;隐私保护成为了公众关注的焦点&#xff0c;尤其是对于拥有数十亿用户的社交媒体巨头 Facebook 来说&#xff0c;其隐私保护措施的有效性更是备受瞩目。本文将从技术角度探讨 Facebook 的隐私保护措施是否足够。 数据收集与使用 Facebook 收集用户数据的…

多语种OCR识别系统,引领文字识别新时代

在全球化与数字化深度融合的今天&#xff0c;语言障碍成为企业跨国协作、信息管理的一大挑战。无论是跨国合同签署、多语言档案管理&#xff0c;还是跨境商务沟通&#xff0c;高效精准的文字识别技术已成为刚需。中安智能OCR多语种识别系统应运而生&#xff0c;凭借其强大的光学…

强化学习实战:训练AI玩转OpenAI Gym

强化学习实战&#xff1a;训练AI玩转OpenAI Gym 系统化学习人工智能网站&#xff08;收藏&#xff09;&#xff1a;https://www.captainbed.cn/flu 文章目录 强化学习实战&#xff1a;训练AI玩转OpenAI Gym摘要引言强化学习基础与算法分类1. 核心概念与数学表示2. 算法分类与…

前端实现导出element-plus表格和从后端获取数据导出,支持勾选导出

1. 纯前端实现导出 安装file-saver和xlsx file-saver&#xff1a; 用于在浏览器中触发文件的保存下载&#xff08;保存为本地文件&#xff09;。 使用场景&#xff1a; 当已经在 JavaScript 中生成了文件&#xff08;如 Blob 对象&#xff09;&#xff0c;并想让用户保存它时使…

Stable Diffusion学习指南【ControlNet上篇】- 功能介绍、安装和使用

&#xff08;注&#xff1a;文末扫码获取AI工具安装包和AI学习资料&#xff09; 自 SD 系列教程发布这几个月&#xff0c;已被大家多次催更 ControlNet 的教程&#xff0c;相信很多朋友也都听说过这款神奇的控图工具。ControlNet 到底是什么&#xff1f;为什么作为一款插件它可…

论文阅读 | CVPR | MambaOut:视觉任务真的需要 Mamba 吗?

文章目录 论文阅读 | CVPR | MambaOut&#xff1a;视觉任务真的需要 Mamba 吗&#xff1f;摘要引言创新点概念讨论Mamba到底适合处理什么样的任务&#xff1f;视觉任务具有很长的序列吗&#xff1f;如何计算Transformer 模块的浮点运算次数&#xff08;FLOPs&#xff09;?定义…