【linux线程(三)】生产者消费者模型详解(多版本)

news2025/1/16 3:43:46

💓博主CSDN主页:杭电码农-NEO💓

⏩专栏分类:Linux从入门到精通⏪

🚚代码仓库:NEO的学习日记🚚

🌹关注我🫵带你学更多操作系统知识
  🔝🔝


在这里插入图片描述

Linux线程

  • 1. 前言
  • 2. 初识生产者消费者模型
  • 3. 阻塞队列版本的代码实现
  • 4. 初识posix信号量
  • 5. 基于信号量的环形队列模型
  • 6. 环形队列模型的代码编写
  • 7. 总结以及拓展

1. 前言

学习进程和线程也很久了,它们具体能解决
什么问题?有什么实际的运用?

本章重点:

本篇文章着重讲解基于多线程下的
生产者消费者模型的概念以及实现.
不仅如此,文章还会拓展基于使用信号量
实现的环形队列版的生产者消费者模型


2. 初识生产者消费者模型

先回答第一个问题:
什么是生产者消费者模型?

生产者消费者模型的本质就是通过一个阻塞队列来将生产者和消费者解耦合的模型.可以将这个模型比作一个超时,生产者生产了数据(商品)后,不会直接拿给消费者使用,而是将数据(商品)先加入到超市,让超市帮我们卖数据(商品).同理,对于消费者来说它并不会直接去生产者的厂家直接拿数据(商品),而是去超市中购买数据(商品).这样生产者和消费者两个对象就解耦合了

再回答第二个问题:
这个模型有什么好处?

想象一下这个场景,假如没有超市的存在,消费者去买物品的伤害要直接到生产厂家去买,并且消费者还不知道生产厂家现在是否有物品,很容易跑空.对于生产者来说,他也不知道消费者需要哪些物品,也就不好生产,容易生产出来的物品没人问津,加入超市这个角色就可以解决上面的问题.综上所述,生产者消费者模型不仅仅将二者解耦了,并且还提高了效率,不会让两方出现跑空的情况(超市没有商品了,消费者最清楚,超市有商品时,生产者最清楚)

在这里插入图片描述
不仅如此,生产者消费者模型还支持多个线程一起进入,阻塞队列属于临界资源,所以同一时刻,不管是生产者还是消费者,都只允许一个线程进入到阻塞队列进行push或pop操作.除此之外,我们还需要两个条件变量push_cond和pop_cond,当阻塞队列已满时,push_cond条件变量会让生产者线程等待消费者线程在队列中取走数据,而当阻塞队列为空时,pop_cond条件变量会让消费者线程等待生产者往队列中加入数据,一来一回,也就是push操作会唤醒pop_cond条件变量,而pop操作会唤醒push_cond条件变量


3. 阻塞队列版本的代码实现

在的代码中一定会涉及到加解锁的问题,所以可以根据RAII思想,先设计出一个专门用于加解锁的类:

lockguard.hpp文件:

#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;
class Mutex //封装pthread库的锁
{
public:
    Mutex(pthread_mutex_t* pmtx)
        :_pmtx(pmtx)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmtx);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmtx);
    }
    ~Mutex()
    {
        delete _pmtx;
        _pmtx = nullptr;
    }   
private:
    pthread_mutex_t* _pmtx;
};
//利用RAII思想,用对象的生命周期控制资源
class LockGuard//封装了锁后,出了作用域会自动调用析构函数,解锁!
{
public:
    LockGuard(pthread_mutex_t* mtx)
        :_mtx(mtx)
    {
        _mtx.lock();
    }
    ~LockGuard()
    {
        _mtx.unlock();
    }
private:
    Mutex _mtx;
};

下一步,编写基于阻塞队列的模型,
在实现它之前需要先注意一些点:

下面的define语句可有可无,主要是为了图方便.第二点,检测临界资源是否就绪时要使用while而不是if,因为有时可能会错误的唤醒一个进程,如果是if语句,一旦错误唤醒,就会直接进入临界区,不合理,使用while的话,就算是错误唤醒了一个线程,只要资源不就绪也不能往后走.在下面的代码中并没有使用上上面封装的锁,这是为了方便同学们即使不封装锁也能很好的查看代码,最后,代码的解释都放在注释当中了,如果你还有问题,欢迎你来私信我

block_queue.hpp文件:

#pragma once
#include<iostream>
#include<queue>
#include<pthread.h>
#include"LockGuard.cpp"
using namespace std;
#define INIT_MTX(mtx) pthread_mutex_init(&mtx,nullptr)
#define INIT_COND(cond) pthread_cond_init(&cond,nullptr)
#define DESTORY_MTX(mtx) pthread_mutex_destroy(&mtx)
#define DESTORY_COND(cond) pthread_cond_destroy(&cond)

template<class T>
class BlockQueue
{
public:
    BlockQueue(size_t capacity = 5)
        :_capacity(capacity)
    {
        INIT_MTX(_mtx);
        INIT_COND(_condisempty);
        INIT_COND(_condisfull);
    }
    ~BlockQueue()
    {
        DESTORY_MTX(_mtx);
        DESTORY_COND(_condisempty);
        DESTORY_COND(_condisfull);
    }
    void push(const T& in)
    {
        //1. 加锁
        pthread_mutex_lock(&_mtx);
        //2. 检测临界资源是否满足访问条件,不满足就利用条件变量让它等待
        while(isfull())
        {
            cout<<"队列已满,push等待中..."<<endl;
            //我是抱着锁去等待的,所以即使消费者去pop了,没有锁也只能阻塞等待,就造成死循环了!
            //pthread_cond_wait的第二个参数是一把锁,当成功调用wait后,传入的锁会自动释放!
            //被唤醒时,还是在加锁和解锁之间,wait函数会自动帮线程获取锁
            // 1. wait函数可能会调用失败
            // 2. wait函数可能存在伪唤醒的情况
            // 所以不能使用if语句来判断,而是用while,醒来后再判断一下是不是真的唤醒
            pthread_cond_wait(&_condisfull,&_mtx);//若队列满了,就去isfull变量中等待
            cout<<"等待成功,继续执行..."<<endl;
        }
        //3. 访问临界资源
        _bq.push(in);
        //5. 生产完后唤醒消费者(满足一定条件再唤醒)
        if(_bq.size() >= _capacity/2)
            pthread_cond_signal(&_condisempty);
        //4. 解锁
        pthread_mutex_unlock(&_mtx);
    }
    void pop(T* out)
    {
        //1. 加锁
        pthread_mutex_lock(&_mtx);
        //2. 检测临界资源是否满足访问条件,不满足就利用条件变量让它等待
        while(isempty())
        {
            cout<<"队列为空,pop等待中..."<<endl;
            pthread_cond_wait(&_condisempty,&_mtx);
            cout<<"等待成功,继续执行..."<<endl;
        }
        //3. 访问临界资源
        *out = _bq.front();
        _bq.pop();
        //4. 解锁
        pthread_mutex_unlock(&_mtx);
        //5. 消费完后唤醒生产者
        pthread_cond_signal(&_condisfull);
    }
    bool isempty()
    {
        return _bq.size() == 0;
    }
    bool isfull()
    {
        return _bq.size() ==_capacity;
    }
private:
    queue<T> _bq;
    size_t _capacity;  //阻塞队列容量上限
    pthread_mutex_t _mtx;  //通过互斥锁保证队列push/pop时,不会被pop/push(正在生产时就来消费)
    //push或pop时,怎么知道队列是不是满或空呢?通过条件变量来同步信息
    pthread_cond_t _condisempty; //用它来表示阻塞队列是否为空的条件
    pthread_cond_t _condisfull; //用它来表示阻塞队列是否为满的条件
};

对于这段代码的测试代码较为繁杂,我
把代码的码云链接放出来,有兴趣的同学
下来写完可以去做一些测试:

我的码云链接


4. 初识posix信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

请思考以下问题:

在基于阻塞队列的生产者消费者模型中,生产者关心的是阻塞队列是否满了,而消费者关心的是阻塞队列是否有数据,既然它们关心的资源不同,但进入到阻塞队列就会直接加锁,是不是有一点不合理?有没有一种方法可以只让生产者与生产者之间以及消费者与消费者之间存在锁竞争?而生产者和消费者之间不存在锁竞争?答案是使用信号量!

什么是信号量?
信号量的本质是一个计数器,在使用资源前需要预定资源,也就是让信号量减一.类似于去电影院看电影需要提前买票预定座位,并且在访问完资源释放信号量,也就是让信号量加一

根据以上的信息可以简化模型:

生产者关心队列是否为满,刚开始队列是空,那么生产者的信号量就是n(队列的长度),而消费者关心队列是否有资源,刚开始队列为空,那么消费者的信号量就是0.并且在生产者push一个数据到队列后,生产者的信号量需要减一,而消费者的信号量会加一,同理,消费者从队列拿走一个数据后,生产者的信号量会加一,而消费者的信号量会减一

信号量的使用方法分为以下几步:

  • 初始化信号量
    在这里插入图片描述
  • 销毁信号量

在这里插入图片描述

  • 等待信号量(信号量减一)
    在这里插入图片描述

  • 发布信号量(信号量加一)

在这里插入图片描述

等待和发布信号量的操作又被称为PV操作


5. 基于信号量的环形队列模型

可以优化最初的生产者消费者模型:

使用一个环形队列,只有生产和消费指向环形队列中的同一个位置(队列为满或空)时,才会出现生产者和消费者互斥或同步的问题,而当生产和消费指向环形队列中的不同位置时,只需控制信号量即可

在这里插入图片描述

  • 生产者关心空间资源,使用信号量spacesem,起始值为N
  • 消费者关心数据资源,使用信号量datasem,起始值为0
  • 期望:生产者不能让消费者套圈(为空时要先让生产者先运行)
  • 期望:消费者不能超过生产者(为满时要让消费者先运行)
  • 需要两把锁,一把给生产者之间当互斥锁,另外一把给消费者用

6. 环形队列模型的代码编写

还是和之前一样,可以选择将操作信号量的函数专门写为一个类,当然也可以用原生的

sem.hpp文件中

#pragma once
#include<semaphore.h>
using namespace std;
class Sem
{
public:
    Sem(int value)
    {
        sem_init(&_sem,0,value);
    }
    ~Sem()
    {
        sem_destroy(&_sem);
    }
    void p() //PV操作分别代表信号量减一和信号量加一
    {
        sem_wait(&_sem);
    }
    void v()
    {
        sem_post(&_sem);
    }
private:
    sem_t _sem;
};

在RingQueue.hpp文件中:

#pragma once
#include<iostream>
#include<pthread.h>
#include<vector>
#include<unistd.h>
#include<semaphore.h>
using namespace std;
#include"Sem.hpp"
const int g_num = 5;
template<class T>
class RingQueue
{
public:
    RingQueue(int num = g_num)
        :_num(num)
        ,_rq(num)
        ,_pushindex(0)
        ,_popindex(0)
        ,_space_sem(num)//最开始空间资源是满的
        ,_data_sem(0)//最开始数据资源是空的
    {
        pthread_mutex_init(&pushlock,nullptr);
        pthread_mutex_init(&poplock,nullptr);
    }
    ~RingQueue()
    {
        pthread_mutex_destroy(&pushlock);
        pthread_mutex_destroy(&poplock);
    }
    void push(const T& in)//生产者进行,关心空间资源,生产者们的临界资源是pushindex下标
    {
        _space_sem.p();
        pthread_mutex_lock(&pushlock);//对生产者们进行加锁
        _rq[_pushindex++] = in;
        _pushindex %= _num;
        pthread_mutex_unlock(&pushlock);
        _data_sem.v();
    }
    void pop(T* out)//消费者进行,关心数据资源,消费者们的临界资源也是popindex下标
    {
        //一般先申请信号量,再进行加锁
        _data_sem.p();
        pthread_mutex_lock(&poplock);//对消费者们进行加锁
        //竞争成功的生产者线程只有一个
        *out = _rq[_popindex++];
        _popindex %= _num;
        pthread_mutex_unlock(&poplock);
        _space_sem.v();
    }
private:
    vector<T> _rq;//环形队列可用数组表示
    size_t _num;//总共的元素个数
    size_t _pushindex = 0;//push的下标
    size_t _popindex = 0;//pop的下标
    Sem _space_sem;
    Sem _data_sem;
    pthread_mutex_t pushlock;//生产者的锁
    pthread_mutex_t poplock;//消费者的锁
};

关于代码的解释都在注释中,若你有疑问,欢迎私信我~


7. 总结以及拓展

生产者消费者模型是我们学习线程中的一个很重要的模型,它的思想有助于帮我们解决后续的难题,并且校招时,有可能会让手撕一个生产者消费者模型,所以同学要学扎实了


🔎 下期预告:线程池详解 🔍

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

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

相关文章

【数据结构】哈希表与哈希桶

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.概念 2.哈希冲突…

SylixOS工程如何生成map文件

生成.map文件通常是在编译链接阶段由编译器或链接器自动完成的。如果你需要手动生成.map文件&#xff0c;可以通过配置链接器选项来实现。 以bsp工程为例&#xff0c;在内核工程/libsylixos/SylixOS/mktemp/bsp.mk文件中添加-Wl,-Map,output.map选项来生成.map文件。

Java学习笔记:异常处理

Java学习笔记&#xff1a;异常处理 什么是异常异常体系结构&#xff1a;Error、Exception自定义异常 ​ **2024/3/19** 什么是异常 异常体系结构&#xff1a;Error、Exception 自定义异常

Orange3数据预处理(分类器组件)

创建类属性 从字符串属性创建类属性。 输入 数据&#xff1a;输入数据集 输出 数据&#xff1a;具有新类变量的数据集 功能 创建类属性功能从一个已存在的离散或字符串属性中创建一个新的类属性。该组件匹配所选属性的字符串值&#xff0c;并为匹配的实例构造一个新…

Spring Boot:筑基

Spring Boot 前言概述使用 Intellij idea 快速创建 Spring Boot 项目注意事项 前言 在学习 Spring 、SpringMVC 、MyBatis 和 JPA 框架的过程中&#xff0c;了解到 SSM 框架为 Java Web 开发提供了强大的后端支持&#xff0c;JPA 框架则简化了数据库的操作。然而&#xff0c;S…

【智能算法应用】智能算法优化BP神经网络思路

目录 1.思路2.设计 1.思路 在BP神经网络结构中&#xff0c;权值和阈值被视为模型的参数&#xff0c;它们在训练过程中需要通过反向传播算法进行学习&#xff0c;以使得网络的输出尽可能地接近真实标签。这意味着网络的目标是通过最小化均方误差&#xff08;MSE&#xff09;来调…

Docker专题-03 Log-Driver日志转存

Docker专题教程 注&#xff1a; 本教程由羞涩梦整理同步发布&#xff0c;本人技术分享站点&#xff1a;blog.hukanfa.com 转发本文请备注原文链接&#xff0c;本文内容整理日期&#xff1a;2024-03-19 csdn 博客名称&#xff1a;五维空间-影子&#xff0c;欢迎关注 说明 容器…

echarts饼图图例换行

legend: {left: "5%",bottom: "10%",orient: vertical,}, 完整代码 option {tooltip: {trigger: item},legend: {left: "5%",bottom: "10%",orient: vertical,},// legend: [// {// x: left,// left:"5%",// bottom: …

牛客题霸-SQL进阶篇(刷题记录二)

本文基于前段时间学习总结的 MySQL 相关的查询语法&#xff0c;在牛客网找了相应的 MySQL 题目进行练习&#xff0c;以便加强对于 MySQL 查询语法的理解和应用。 由于涉及到的数据库表较多&#xff0c;因此本文不再展示&#xff0c;只提供 MySQL 代码与示例输出。 部分题目因…

贪心算法(算法竞赛、蓝桥杯)--奶牛晒衣服

1、B站视频链接&#xff1a;A28 贪心算法 P1843 奶牛晒衣服_哔哩哔哩_bilibili 题目链接&#xff1a;奶牛晒衣服 - 洛谷 #include <bits/stdc.h> using namespace std; priority_queue<int> q;//用大根堆维护湿度的最大值 int n,a,b; int tim,maxn;int main(){s…

smodin(Al工具)

一、中文官网 ​​Smodin&#xff1a;多语言写作辅助​​ &#xff08;google账号登录&#xff09; ​​https://smodin.io/zh-cn/​​ 二、具体使用 2.1 写文章 (写 5 个或更多单词、一个问题或一个长标题。标题越好&#xff0c;文章就越好) 选择语言&#xff0c;输入标题…

数据之谜:解读Facebook的用户行为

在当今数字化时代&#xff0c;社交媒体平台已经成为人们生活中不可或缺的一部分&#xff0c;而Facebook作为全球最大的社交网络之一&#xff0c;其背后隐藏着许多数据之谜。本文将深入探讨Facebook的用户行为&#xff0c;并试图解读其中的奥秘。 用户行为数据的收集 Facebook作…

初探Springboot 参数校验

文章目录 前言Bean Validation注解 实践出真知异常处理 总结 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。 前言 工作中我们经常会遇到验证字段是否必填&#xff0c;或者字段的值是否…

网络工程师练习题2

网络工程师 将专用IP地址转换为公用IP地址的技术是&#xff08;&#xff09;。 A.ARPB.DHCPC.UTMD.NAT 【答案】D 【解析】概念题&#xff0c;NAT技术将源地址从内部专用地址转换成可以在外部Internet上路由的全局IP地址。 R1、R2是一个自治系统中采用RIP路由协议的两个相…

微软AI系列 C#中实现相似度计算涉及到加载图像、使用预训练的模型提取特征以及计算相似度

在C#中实现相似度计算涉及到加载图像、使用预训练的模型提取特征以及计算相似度。你可以使用.NET中的深度学习库如TensorFlow.NET来加载预训练模型&#xff0c;提取特征&#xff0c;并进行相似度计算。 以下是一个使用TensorFlow.NET的示例&#xff1a; using System; using …

十二 超级数据查看器 讲解稿 详情7 其他功能

十二 超级数据查看器 讲解稿 详情7 其他功能 点击此处 以新页面 打开B站 播放当前教学视频 点击访问app下载页面 百度手机助手 下载地址 ​ 讲解稿全文&#xff1a; 其他操作&#xff0c;主要用来完成替换和批量修改&#xff0c; 这里&#xff0c;我们想给成语字段增…

HarmonyOS系统开发ArkTS常用组件文本及参数(五)

目录 一、Text组件 1、Text组件案例 二、Text组件参数 1、string字符串类型 2、Resources类型 2.1、resources中内容配置 base/element/string.json 中的内容 zh_US/element/string.json 中的内容 es_US/element/string.json 中的内容 2.2、环境适配 适配英文 适配中文…

2022年安徽省职业院校技能大赛 (高职组)“云计算”赛项样卷

#需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; #需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; #需要资源或有问题的&#xff0c;可私博主&#xff01;&#xff01;&#xff01; 第一场次&#xff1a;私有云(5…

云服务器2核4g能支持多少人同时访问?腾讯云和阿里云PK

腾讯云轻量应用服务器2核4G5M配置性能测评&#xff0c;腾讯云轻量2核4G5M带宽服务器支持多少人在线访问&#xff1f;并发数10&#xff0c;支持每天5000IP人数访问&#xff0c;腾讯云百科txybk.com整理2核4G服务器支持多少人同时在线&#xff1f;并发数测试、CPU性能、内存性能、…

【HMM】Hidden Markov Model

文章目录 1 HMM 的概念1.1 引入1.1.1 Markov property1.1.2 Markov chain1.1.3 一阶离散马尔可夫模型 1.2 HMM 的定义1.3 观测序列的生成过程1.4 HMM 的 3 个基本问题 2 三个基本问题的解法2.1 概率计算算法2.1.1 直接计算法2.1.2 向前算法2.1.3 向后算法2.1.4 一些概率与期望值…