Linux操作系统学习之---基于环形队列的生产者消费者模型(毛坯版)

一.信号量

1.对比:

Systme V标准中有信号量的概念 , POSIX标准中也有 , 他俩仅有细微的差别

  • Systme V标准中的信号量用于进程间的同步.
  • POSIX标准中的信号量默认用于线程间的同步.

二者本质是相同的 , 都是一个计数器 , 代表有多少剩余资源可以使用 .

2.POSIX信号量函数接口:

头文件 : <semaphore.h>

sem_init:

int sem_init(sem_t *sem, int pshared, unsigned int value);

  • 返回值 : 成功为0 , 失败为-1

  • 参数一 : 信号量类型sem_t的指针

  • 参数二 : 模式 , 0为线程间共享 , 1为进程间共享(因为Linux下线程是轻量级进程,二者界限比较模糊)

  • 参数三 : 规定最大容纳多少个资源.

  • 调用逻辑: 初始化一个信号量变量.

sem_wait:

int sem_wait(sem_t *sem);

  • 返回值 : 成功为0 , 失败为-1;

  • 参数 : 信号量类型sem_t的指针

  • 调用逻辑 : 类似于计数器-- , 只不过这个过程是原子的!!! 若计数器归零 , 开始等待.

sem_post:

int sem_post(sem_t *sem);

  • 返回值 : 成功为0 , 失败为-1;

  • 参数一 : 信号量类型sem_t的指针

  • 调用逻辑 : 类似于计数器++ , 过程也是原子的!!! 若计数器的值达到上限 , 开始等待.

sem_destroy:

int sem_destroy(sem_t *sem);

  • 返回值 : 成功为0 , 失败为-1

  • 参数一 : 信号量类型sem_t的指针

  • 调用逻辑: 销毁一个信号量变量.


二.基于RingQueue和Sem的C_P模型代码实现

1.环形队列:

  • 作为一种数据结构 , 在保证先进先出 的性质下 , 还能复用旧空间 .
  • 先进先出 正好符合线程互斥的原则.
  • 旧空间的复用 正好符合多线程并发的需求.
    使用vector作为环形作为底层容器
    !vector作为RingQueue底层容器的依据-
  1. 首先 , vector随机访问效率很高 ,不用说 .
  2. 最重要的是 , vector的下标访问 天然满足并发需求!!!
    *生产者和消费者可以通过vector下标独立的访问容器的不同位置 , 不会被诸如List底层的指针限制 . *
    #Linux/线程/vector_RingQueue

其中,信号量就类似于避免头尾指针在运行过程中相遇所使用的计数器 , 因此不用额外开辟一个空间

2.基本类(RingQueue)框架 :

成员变量:

  • vector<T> _ringQueue:消费者和生产者都可以访问的交易市场(共享资源).
  • int _cap : 人为规定的共享资源的最大容量 .
  • Sem _csem : 消费者的信号量 , 记录剩下多少可使用资源.
  • Sem _psem : 生产者的信号量 , 记录剩下多少空闲位置.
  • int _cstep : 消费者的消费位置 , 即向vector里添加元素时的下标.
  • int _pstep : 生产者的生产位置 , 即向vector里获取元素时的下标.
    成员函数:
  • RingQueue() : 构造函数 , 除了初始化成员变量外 , 也调用resize()为vector提前开辟好空间
  • void Consume() : 消费者消费一次的逻辑.
  • void Produce(const T &product) : 生产者生产一次的逻辑.

3.单生产,单消费:

RingQueue:

c++ 复制代码
#pragma once
#include<vector>
#include"Cond.hpp"
#include"Mutex.hpp"
#include"Sem.hpp"

using namespace Cond_module;
using namespace Mutex_module;
using namespace Semaphore_module;

template<typename T>
class RingQueue
{
    public:
    static const int max_cap = 5;
    RingQueue()
    :_cap(max_cap)
    ,_cstep(0)
    ,_pstep(0)
    ,_csem(0)
    ,_psem(max_cap)
    {
        _ringQueue.resize(_cap);
    }
    void Consume()
    {
        _csem.Wait();

        auto ret = _ringQueue[_cstep++];

        _cstep %= _cap;

        _psem.Post();
    }
    void Produce(const T& product)
    {
        _psem.Wait();
        
        auto ret = _ringQueue[_pstep++];//Linux下非得接受下标访问的返回值
        (void)ret; //但是为了避免返回值没被使用的警告,强行使用!!!

        _pstep %= _cap;

        _csem.Post();
    }
    ~RingQueue()
    {

    }
private:
    std::vector<T> _ringQueue;
    int _cap;

    // int _surplus;
    // int _space;
    Sem _csem;
    Sem _psem;

    int _cstep;//消费者的消费位置
    int _pstep;//生产者的生产位置

    Sem _sem; //确保生产者和消费者的同步关系;
};

Main.cpp

c++ 复制代码
#include"RingQueue.hpp"

#include<unistd.h>

template<typename T>
void* Consumer(void* input)
{
    auto rq = static_cast<RingQueue<T>*>(input);
    while(true)
    {
        sleep(1);
        rq->Consume();
        std::cout << "Consume()!!!\n" ;
    }
    return nullptr;
}

template<typename T>
void* Producer(void* input)
{
    auto rq = static_cast<RingQueue<T>*>(input);
    while(true)
    {
        rq->Produce(1);
        std::cout << "Producer()!!!\n" ;
    }
    
    return nullptr;
}

int main()
{
    
    pthread_t tid1 = 0;
    pthread_t tid2 = 0;
    RingQueue<int>* rq = new RingQueue<int>;

    pthread_create(&tid1,nullptr,Consumer<int>,(void*)rq);


    pthread_create(&tid1,nullptr,Producer<int>,(void*)rq);

    while(true) { sleep(1); }

    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    return 0;
}

4.多生产,多消费:

单生产单消费中以及实现了消费者和生产者 之间的同步 , 接下来是消费者之间 以及生产者之间的同步.
加上两把锁 即可 : Mutex _cmut 和 Mutex_pmut

一把用于消费者之间 , 另一把用于生产者之间

RingQueue(加入两把锁即可):

c++ 复制代码
void Consume()
    {
        _csem.Wait();
        {
            LockGuard lg(_cmut); //RAII风格的互斥锁

            auto ret = _ringQueue[_cstep++];

            _cstep %= _cap;

            _psem.Post();
        }
    }
    void Produce(const T &product)
    {
        _psem.Wait();
        {
            LockGuard lg(_pmut);//RAII风格的互斥锁

            auto ret = _ringQueue[_pstep++];
            (void)ret;

            _pstep %= _cap;

            _csem.Post();
        }
    }

锁和信号量的先后???

上面的代码里将加锁放在信号量的等待之前是有讲究的 .

可以这样理解:

  • 锁保证了线程间串行 , 就好像电影院门口排队的人群 ;

  • 信号量保证了线程间数据的一致性 , 就好像排队人群手里的票;

    复制代码
        **先买票了再去排队好,还是先排队后再买票???**
        **答案毋庸置疑是 : 先买票了在排队 .**
  • 同理 : 让线程先通过信号量预定资源的使用权 (买票) , 等拿到锁排队到自己 后才能真正使用资源 (进入电影院) .从而提高效率.

三. BlockQueue和RingQueue的本质差别:

1.底层数据结构 :

对于BlockQueue :

  • 底层咱用的是容器适配器 queue , 而他的底层又封装了容器de_queue .

  • 对数据的修改本质是通过底层维护的指针 , 而指针没法顺应生产者和消费者二者异步访问的需求 . 因为没法让指针在一个数据结构里指来指去.

  • 因此只能把公共资源看做一个整体 .
    对于RingQueue :

  • 底层咱用的是容器 vector , 它的本质是一个更灵活的数组 .

  • 此时对数据的修改取决于生产者和消费者传入的下标 , 可以异步访问 . 因为使用不同的下标自然访问不通的位置.

  • 因此可以把公共资源划分成很多部分.

2.条件变量和信号量:

对于条件变量Cond:

  • 只能允许一方访问 --- 生产者或者消费者 , 以至于二者不能同时进行.
    对于信号量semaphore

  • 可以允许两方访问 --- 生产者和消费者异步进行 , 各自的信号量又保证了在访问条件不满足时进行等待
    转化!!!

只要信号量中的资源数量设置为1 , 那就退化成了条件变量!!!

相关推荐
kebidaixu4 小时前
BCU 平台 RS485 驱动适配:从 THVD1406 到 ISO3082
linux
V搜xhliang02464 小时前
AI智能体的数据安全与合规实践
人工智能·学习·数据分析·自动化·ai编程
unicrom_深圳市由你创科技4 小时前
哪些控制逻辑应该放在 PLC,哪些放在上位机?
c++
无敌的牛5 小时前
redis学习过程
数据库·redis·学习
玖玥拾6 小时前
C/C++ 基础笔记(十三)继承
c语言·c++·继承
谢平康7 小时前
解决用 rm 报bash: /usr/bin/rm: Argument list too long错
linux·运维·运维开发
旅僧7 小时前
Π环境部署(运行 且 无理论讲解)
学习
jushi89997 小时前
Lucas Chess R国际象棋、中国象棋、日本将棋、五子棋训练学习工具游戏软件
学习
ao-weilai7 小时前
C++:哈希表
c++·哈希算法·散列表
汉克老师7 小时前
GESP7级C++考试语法知识(二、指数函数(1、pow() 函数)
c++·指数函数·pow·gesp7级·精度误差