【Linux系统】生产者消费者模型:基于环形队列(信号量机制)




在这里插入图片描述



理论层面

1、环形队列的特性认识

  • 环形队列采用数组模拟,用模运算来模拟环状特性

在这里插入图片描述


  • 环形结构起始状态和结束状态都是⼀样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留⼀个空的位置,作为满的状态

在这里插入图片描述




2、环形队列的生产消费模型

对于计算机而言,本质上只有两种资源:空间和数据。

  • 环形队列的生产消费模型可以理解为一种固定存放数据的空间机制。在这个模型中,生产者申请空位置写入数据,而消费者从非空的位置获取数据。

在这里插入图片描述




3、环形队列的特性

  1. 互斥
    当环形队列为“空”时,只有生产者能够进入并写入数据;当环形队列为“满”时,只有消费者能够进入并读取数据。这种机制保证了生产者和消费者之间的互斥性。

  2. 同步
    当环形队列为“空”时,必须让生产者先进行生产操作;当环形队列为“满”时,必须让消费者先进行消费操作。这种机制体现了生产者和消费者之间的同步关系。


4、并发与追逐游戏

在环形队列不为空也不为满的情况下,生产者和消费者的关系就像一场在圆桌上的追逐游戏:

  • 生产者不停地绕着圆桌,在每个空盘子上放置苹果。
  • 消费者则紧跟其后,不停地从盘子中拿走苹果。

在这种情况下,生产者和消费者的操作是并发的,彼此独立且互不影响。


5、特殊情况

  • 当消费者追上生产者时,说明消费者的消费能力较强,很快将圆桌上的苹果全部拿完,此时环形队列处于“空”的状态。
  • 当生产者追上消费者时,说明生产者的生产能力较强,很快将圆桌上的苹果全部放满,此时环形队列处于“满”的状态。

由于环形队列的“空”和“满”状态会触发互斥与同步机制,因此消费者绝对不可能超越生产者。


6、资源需求与信号量设计

  • 对于生产者来说,需要的是空间资源,因为生产者需要向空的位置写入数据。
  • 对于消费者来说,需要的是物品资源,因为消费者需要从非空位置读取数据。

基于此,可以设计两个信号量:

  1. 空间信号量:用于表示可用的空闲空间。
  2. 物品信号量:用于表示已有的数据资源。

综上所述,生产者和消费者在同一位置时体现同步和互斥,而在不同位置时体现并发关系。



代码层面

1、生产者与信号量的操作逻辑

  1. 生产者对空间的 P 操作
    生产者需要占用一个空闲空间来写入数据。因此,在生产之前,它会对空间信号量进行 P 操作(申请资源)。这表示生产者正在尝试获取一个可用的空间。

  2. 生产者对数据的 V 操作
    当生产者完成数据写入后,它并不会释放自己占用的空间(即不会对空间信号量进行 V 操作),而是通过 V 操作增加数据信号量的值。这是因为生产者的核心任务是生成数据,并将数据提供给消费者使用。

    • 空间信号量:表示可用的空闲空间数量。
    • 数据信号量:表示已生成的数据数量。

    因此,生产者的操作逻辑可以总结为:

    • 对空间信号量 P 操作:申请一个空闲空间。
    • 对数据信号量 V 操作:通知消费者有新的数据可以消费。



2、信号量的本质与作用

信号量的作用并不是直接控制生产者或消费者的具体位置,而是通过数值的变化来控制生产和消费的次数。换句话说,生产者和消费者只需要检查各自信号量的值,就能判断是否可以进入临界区执行操作。

  • 空间信号量:控制生产者的生产次数。当空间信号量的值大于 0 时,生产者可以进入临界区;否则必须等待。
  • 数据信号量:控制消费者的消费次数。当数据信号量的值大于 0 时,消费者可以进入临界区;否则必须等待。

通过这种机制,信号量实现了以下功能:

  1. 同步:确保生产者和消费者按照正确的顺序操作。例如,当队列为空时,生产者优先生产;当队列为满时,消费者优先消费。
  2. 互斥:避免生产者和消费者同时访问同一个位置,从而保护共享资源的完整性。



3、程序雏形:单生产者单消费者

RingBuffer.hpp
#ifndef _RING_BUFFER_HPP_
#define _RING_BUFFER_HPP_

#include <iostream>
#include <queue>
#include <pthread.h>
#include <vector>
#include "Cond.hpp"
#include "Mutex.hpp"
#include "Sem.hpp"

using namespace CondModule;
using namespace MutexModule;
using namespace SemModule;

// 使用封装的条件变量和互斥锁
namespace RingBufferModule
{
    // 数据
    int num = 10;

    template <typename T>
    class RingBuffer
    {
    public:
        RingBuffer(size_t cap = 10)
            : _cap(cap), _ring(cap), _producer_index(0), _consumer_index(0), _space(_cap), _data(0)
        {
        }
        ~RingBuffer()
        {
        }

        void Equeue(const T &in)
        {
            // 生产者生产数据, 生产者争夺锁用于争夺信号量的使用权
            _space.P();

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

            _data.V();
        }

        void Pop(T *out)
        {
            // 消费者消费数据, 消费者争夺锁用于争夺信号量的使用权
            _data.P();

            *out = _ring[_consumer_index];
            _consumer_index++;
            _consumer_index %= _cap;

            _space.V();
        }

    private:
        std::vector<T> _ring;
        int _cap;            // 容量
        int _producer_index; // 生产者下标
        int _consumer_index; // 消费者下标

        Sem _space;
        Sem _data;
    };
}

#endif



Main.cc
#include "RingBuffer.hpp"
#include "Task.hpp"
#include "Sem.hpp"
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <functional>

using namespace RingBufferModule;
using namespace SemModule;



void *Producer(void *args)
{
    
    RingBuffer<int>* ring_buffer = static_cast<RingBuffer<int>*>(args);
    int data = 0;
    while (true)
    {
        ring_buffer->Equeue(data);
        std::cout << "生产者生产了一个数据: " << data++ << '\n';
    }
}


void *Consumer(void *args)
{
    RingBuffer<int>* ring_buffer = static_cast<RingBuffer<int>*>(args);
    while (true)
    {
        sleep(1);
        int data;
        ring_buffer->Pop(&data);
        std::cout << "消费者消费了一个数据: " << data << '\n';
    }
}


int main()
{
    srand(time(0) ^ getpid());
    RingBuffer<int> ring_buffer;

    // 创建生产者线程
    pthread_t _tid1;
    pthread_create(&_tid1, nullptr, Producer, &ring_buffer);


    // 创建消费者线程
    pthread_t _tid10;
    pthread_create(&_tid10, nullptr, Consumer, &ring_buffer);

    
    // 回收线程
    pthread_join(_tid1, nullptr);
    pthread_join(_tid10, nullptr);


    return 0;
}


Sem.hpp :对信号量的封装
#pragma once

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

namespace SemModule
{
    class Sem
    {
    public:
        Sem(int value)
            : _init_val(value)
        {
            sem_init(&_sem, 0, _init_val);
        }
        ~Sem()
        {
            sem_destroy(&_sem);
        }

        // P操作
        void P()
        {
            sem_wait(&_sem);
        }

        // V操作
        void V()
        {
            sem_post(&_sem);
        }

    private:
        sem_t _sem;
        int _init_val;
    };
}

运行结果如下,可以看到消费者正按照一定顺序逐个消费数据

在这里插入图片描述


以下是代码设计的相关讨论:


为什么信号量代码中没有判断是否为空或为满?


1. 锁的作用

在使用互斥锁的代码中,我们需要手动判断队列是否为空或为满:

  • 锁的作用只是控制对临界区的访问权限。
  • 锁并不知道临界区内资源(数据)的具体状态。因此,我们必须通过额外的逻辑来判断队列的状态(空或满),以决定是否允许生产或消费。
  • 锁只是一个看门的,至于门里的数据什么情况,和锁无关


2. 信号量的作用

信号量本身的设计已经包含了对资源数量的记录和管理:

  • 空间信号量:表示队列中有多少个空闲位置。
  • 数据信号量:表示队列中有多少个已生成的数据。

当信号量的操作成功时(P 操作通过),说明当前确实有可用资源,无需再单独判断队列是否为空或为满。信号量机制直接将资源的管理内化到其操作逻辑中,简化了代码。

总结来说:

  • 锁需要判断空满是因为它只负责访问控制,不了解资源状态。
  • 信号量不需要判断空满是因为它的初始值和操作本身就反映了资源的状态。



单生产者单消费者 vs 多生产者多消费者


1、单生产者单消费者

在这种情况下,主要需要解决的是生产者和消费者之间的同步与互斥关系

  • 同步:确保生产者和消费者按照正确的顺序操作(生产者先生产,消费者后消费)。
  • 互斥:避免生产者和消费者同时访问同一个位置。

信号量机制可以很好地解决这些问题:

  • 使用空间信号量控制生产者的生产次数。
  • 使用数据信号量控制消费者的消费次数。


2、多生产者多消费者

在这种情况下,除了生产者和消费者之间的同步与互斥关系外,还需要解决以下问题:

  1. 生产者之间的互斥
    • 多个生产者可能同时尝试写入队列中的某个位置,因此需要保证生产者之间的互斥。
  2. 消费者之间的互斥
    • 多个消费者可能同时尝试读取队列中的某个位置,因此需要保证消费者之间的互斥。



如何解决多生产者多消费者的互斥问题:加锁

1. 是否可以用一把锁?

理论上可以用一把锁来解决所有问题,但这会导致 生产和消费被完全串行化

  • 生产者和消费者都需要等待对方释放锁才能继续操作。
  • 这种方式会 严重影响程序的并行性,失去了信号量机制的优势

此外, 若只定义了一把锁可能会导致死锁问题:

  • 生产者先获取锁,然后阻塞在对空间的 P 操作上,而某个消费者需要锁进来入临界区代码进行空间的 V 操作,生产者只有获取该消费者给的空间才能继续执行代码,造成了死锁。


2. 应该用几把锁?

建议使用两把锁:

  • 生产者锁:用于保证多个生产者之间的互斥。
  • 消费者锁:用于保证多个消费者之间的互斥。

这种设计既能保证生产者和消费者之间的同步,又能保证生产者之间和消费者之间的互斥,同时不会完全串行化生产和消费操作。

3. 加锁的目的

  • 加锁的目的:每次进行生产或消费时,生产者或消费者需要争抢得出一个代表,进入临界区进行生产或消费。
  • 有了锁之后,多生产者和多消费者的情况本质上变成了单生产者和单消费者的情况,因为读写位置只有一个!



锁与信号量的结合使用

我想了想,本来想着用锁锁住生产者和消费者对队列下标的使用权,但经过思考后发现:

  • 如果已经通过信号量成功申请到了资源(即进入了临界区),说明当前确实有可用的空间或数据,无需再争抢下标。
  • 因此,锁的作用可以转换为争夺信号量的使用权:多个线程同时抢到了一个信号量,我们需要选出一个代表进行真正的生产或消费工作,这就是用锁控制多线程对信号量的使用权。



锁加在信号量之前还是之后?

后锁

  • 因为多个线程可以对同一个信号量进行 P 操作,所以会出现多线程同时拥有生产或消费权力的情况。
  • 我们设计的锁本质上是为了控制对空间或物资的使用权,而空间或物资的使用权本质上就是对相应信号量的使用权。
  • 因此,可以将锁加在信号量的 P 操作之后,表示每个线程对信号量进行 P 操作后,还需要获取锁,才能真正进入临界区进行生产或消费。
  • 这样,后锁也就保证了互斥关系,保证每次只能有一个生产者进入临界区或只能有一个消费者进入临界区
void Equeue(const T& in)
{
    // 生产者生产数据, 生产者争夺锁用于争夺信号量的使用权
    _space.P();
    _p_mux.lock();

    //...临界区

    _p_mux.unlock();
    _data.V();
}

void Pop(T* out)
{
    // 消费者消费数据, 消费者争夺锁用于争夺信号量的使用权
    _data.P();
    _c_mux.lock();

    //...临界区

    _c_mux.unlock();
    _space.V();
}


先锁

所有线程必须先获取锁,才能进一步申请信号量,进而进入临界区生产或消费

这个过程,也保证了多生产者之间和多消费者之间的互斥关系



先锁好还是后锁好?

无论先锁还是后锁,都能保证互斥关系,但后锁更优:对于同一个信号量,多线程可以并行的申请!若前锁,则导致获取锁和获取信号量都是串行的,影响效率;若后锁,则可以显然信号量被所有线程并行的申请,无需串行的等待对方,如此提高了一定效率



因此一般都是先信号量,再加锁!

void Equeue(const T& in)
{
    // 生产者生产数据, 生产者争夺锁用于争夺信号量的使用权
    _space.P();
    _p_mux.lock();

    //...

    _p_mux.unlock();
    _data.V();
}

void Pop(T* out)
{
    // 消费者消费数据, 消费者争夺锁用于争夺信号量的使用权
    _data.P();
    _c_mux.lock();

    //...

    _c_mux.unlock();
    _space.V();
}


用上 LockGuard 让代码更加优雅一点!

void Equeue(const T &in)
{
    // 生产者生产数据, 生产者争夺锁用于争夺信号量的使用权
    _space.P();

    {
        LockGuard p_lock(_p_mux);
        _ring[_producer_index] = in;
        _producer_index++;
        _producer_index %= _cap;
    }

    _data.V();
}

void Pop(T *out)
{
    // 消费者消费数据, 消费者争夺锁用于争夺信号量的使用权
    _data.P();

    {
        LockGuard c_lock(_c_mux);
        *out = _ring[_consumer_index];
        _consumer_index++;
        _consumer_index %= _cap;
    }

    _space.V();
}


因此最终代码如下:



完整版代码

#ifndef _RING_BUFFER_HPP_
#define _RING_BUFFER_HPP_

#include <iostream>
#include <queue>
#include <pthread.h>
#include <vector>
#include "Cond.hpp"
#include "Mutex.hpp"
#include "Sem.hpp"

using namespace CondModule;
using namespace MutexModule;
using namespace SemModule;

// 使用封装的条件变量和互斥锁
namespace RingBufferModule
{
    // 数据
    int num = 10;

    template <typename T>
    class RingBuffer
    {
    public:
        RingBuffer(size_t cap = 10)
            : _cap(cap), _ring(cap), _producer_index(0), _consumer_index(0), _space(_cap), _data(0)
        {
        }
        ~RingBuffer()
        {
        }

        void Equeue(const T &in)
        {
            // 生产者生产数据, 生产者争夺锁用于争夺信号量的使用权
            _space.P();

            {
                LockGuard p_lock(_p_mux);
                _ring[_producer_index] = in;
                _producer_index++;
                _producer_index %= _cap;
            }

            _data.V();
        }

        void Pop(T *out)
        {
            // 消费者消费数据, 消费者争夺锁用于争夺信号量的使用权
            _data.P();

            {
                LockGuard c_lock(_c_mux);
                *out = _ring[_consumer_index];
                _consumer_index++;
                _consumer_index %= _cap;
            }

            _space.V();
        }

    private:
        std::vector<T> _ring;
        int _cap;            // 容量
        int _producer_index; // 生产者下标
        int _consumer_index; // 消费者下标

        // 锁
        Mutex _p_mux; // 生产者锁
        Mutex _c_mux; // 消费者锁

        Sem _space;
        Sem _data;
    };
}

#endif

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【笔记】LLM|Ubuntu22服务器极简本地部署DeepSeek+API使用方式

2025/02/18说明&#xff1a;2月18日~2月20日是2024年度博客之星投票时间&#xff0c;走过路过可以帮忙点点投票吗&#xff1f;我想要前一百的实体证书&#xff0c;经过我严密的计算只要再拿到60票就稳了。一人可能会有多票&#xff0c;Thanks♪(&#xff65;ω&#xff65;)&am…

leetcode-414.第三大的数

leetcode-414.第三大的数 code review! 文章目录 leetcode-414.第三大的数一.题目描述二.代码提交 一.题目描述 二.代码提交 class Solution { public:int thirdMax(vector<int>& nums) {set<int> set_v(nums.begin(), nums.end());auto it set_v.rbegin()…

【设计模式】 代理模式(静态代理、动态代理{JDK动态代理、JDK动态代理与CGLIB动态代理的区别})

代理模式 代理模式是一种结构型设计模式&#xff0c;它提供了一种替代访问的方法&#xff0c;即通过代理对象来间接访问目标对象。代理模式可以在不改变原始类代码的情况下&#xff0c;增加额外的功能&#xff0c;如权限控制、日志记录等。 静态代理 静态代理是指创建的或特…

深度学习之图像回归(二)

前言 这篇文章主要是在图像回归&#xff08;一&#xff09;的基础上对该项目进行的优化。&#xff08;一&#xff09;主要是帮助迅速入门 理清一个深度学习项目的逻辑 这篇文章则主要注重在此基础上对于数据预处理和模型训练进行优化前者会通过涉及PCA主成分分析 特征选择 后…

利用分治策略优化快速排序

1. 基本思想 分治快速排序&#xff08;Quick Sort&#xff09;是一种基于分治法的排序算法&#xff0c;采用递归的方式将一个数组分割成小的子数组&#xff0c;并通过交换元素来使得每个子数组元素按照特定顺序排列&#xff0c;最终将整个数组排序。 快速排序的基本步骤&#…

照片模糊怎么变清晰?图生生AI修图-一键清晰放大

当打开手机相册时&#xff0c;那些泛着噪点的合影、细节模糊的风景照、像素化的证件图片&#xff0c;让珍贵时刻蒙上遗憾的面纱。而专业级图像修复工具的门槛&#xff0c;让多数人只能无奈接受这些"不完美的记忆"。AI技术的发展&#xff0c;让普通用户也能轻松拥有专…

Linux 网络与常用操作(适合开发/运维/网络工程师)

目录 OSI 七层协议简介 应用层 传输层 Linux 命令&#xff01;&#xff01;&#xff01; 1. ifconfig 命令 简介 1. 查看网络地址信息 2. 指定开启、或者关闭网卡 3. 修改、设置 IP 地址 4. 修改机器的 MAC 地址信息 5. 永久修改网络设备信息 2. route 路由命令 …

PID控制学习

前言 本篇文章属于PID控制算法的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 PID入门教程-电机控制 倒立摆 持续更新中_哔哩哔哩_bilibili 一…

第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式

第1期 定时器实现非阻塞式程序 按键控制LED闪烁模式 解决按键扫描&#xff0c;松手检测时阻塞的问题实现LED闪烁的非阻塞总结补充&#xff08;为什么不会阻塞&#xff09; 参考江协科技 KEY1和KEY2两者独立控制互不影响 阻塞&#xff1a;如果按下按键不松手&#xff0c;程序就…

【Arxiv 大模型最新进展】PEAR: 零额外推理开销,提升RAG性能!(★AI最前线★)

【Arxiv 大模型最新进展】PEAR: 零额外推理开销&#xff0c;提升RAG性能&#xff01;&#xff08;★AI最前线★&#xff09; &#x1f31f; 嗨&#xff0c;你好&#xff0c;我是 青松 &#xff01; &#x1f308; 自小刺头深草里&#xff0c;而今渐觉出蓬蒿。 NLP Github 项目…

vscode的一些实用操作

1. 焦点切换(比如主要用到使用快捷键在编辑区和终端区进行切换操作) 2. 跳转行号 使用ctrl g,然后输入指定的文件内容&#xff0c;即可跳转到相应位置。 使用ctrl p,然后输入指定的行号&#xff0c;回车即可跳转到相应行号位置。

OAI 平台 4G(LTE)基站 、终端、核心网 端到端部署实践(一)

本系列文章,基于OAI LTE代码搭建端到端运行环境,包含 eNB,EPC,UE三个网元。本小节先介绍系统总体架构,硬件平台及驱动安装方法。 1. Overview 系统总体架构如下图所示。 2 Machine setup 2.1 Machine specs Distributor ID: Ubuntu Description: Ubuntu 18.04.5 LTS…

Linux环境Docker使用代理推拉镜像

闲扯几句 不知不觉已经2月中了&#xff0c;1个半月忙得没写博客&#xff0c;这篇其实很早就想写了&#xff08;可追溯到Docker刚刚无法拉镜像的时候&#xff09;&#xff0c;由于工作和生活上的事比较多又在备考软考架构&#xff0c;拖了好久…… 简单记录下怎么做的&#xf…

基于TI的TDA4高速信号仿真条件的理解 4.6

Application Note 《Jacinto7 AM6x, TDA4x, and DRA8x High-Speed Interface Design Guidelines》 4.6 Reviewing Simulation Results检查仿真结果 The results generated by the channel simulations outlined in the preceding sections are compared against an eye mask s…

unity学习46:反向动力学IK

目录 1 正向动力学和反向动力学 1.1 正向动力学 1.2 反向动力学 1.3 实现目标 2 实现反向动力 2.1 先定义一个目标 2.2 动画层layer&#xff0c;需要加 IK pass 2.3 增加头部朝向代码 2.3.1 专门的IK方法 OnAnimatorIK(int layerIndex){} 2.3.2 增加朝向代码 2.4 …

DeepSeek 和 ChatGPT 在特定任务中的表现:逻辑推理与创意生成

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;Linux网络编程 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f389;欢迎大家点赞&#x1f44d;评论&#x1f4dd;收藏⭐文章 ​ Linux网络编程笔记&#xff1a; https://blog.cs…

DAY07 Collection、Iterator、泛型、数据结构

学习目标 能够说出集合与数组的区别数组:1.是引用数据类型的一种2.可以存储多个元素3.数组的长度是固定的 int[] arr1 new int[10]; int[] arr2 {1,2,3};4.数组即可以存储基本类型的数据,又可以存储引用数据类型的数据int[],double[],String[],Student[]集合:1.是引用数据类…

ls命令的全面参数解析与详尽使用指南

目录 ls 命令的所有参数及含义 ls -a 命令详解 ls -A 命令详解 ls -b 命令详解 ls -C 命令详解 ls -d 命令详解 ls -f 命令详解 ls -F 命令详解 ls -G 命令详解 ls -h 命令详解 ls -H 命令详解 ls -i 命令详解 ls -I 命令详解 ls -l 命令详解 ls -L 命令详解 l…

【Spring+MyBatis】_图书管理系统(中篇)

【SpringMyBatis】_图书管理系统&#xff08;上篇&#xff09;-CSDN博客文章浏览阅读654次&#xff0c;点赞4次&#xff0c;收藏7次。&#xff08;1&#xff09;当前页的内容records&#xff08;类型为List&#xff09;&#xff1b;参数&#xff1a;userNameadmin&&pas…

动态规划算法篇:枚举的艺术

那么本篇文章就正式进入了动态规划的算法的学习&#xff0c;那么动态规划算法也可谓是算法内容中的一座大山&#xff0c;那么在大厂算法笔试乃至算法比赛中出现的频率也逐渐变高&#xff0c;那么可见学习好动态规划算法的一个重要性&#xff0c;那么对于动态规划最难理解的&…