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 , 那就退化成了条件变量!!!

相关推荐
dxnb223 小时前
Datawhale25年10月组队学习:math for AI+Task5解析几何
人工智能·学习
哲Zheᗜe༘3 小时前
了解学习Redis主从复制
数据库·redis·学习
南林yan5 小时前
Debian、Ubuntu、CentOS:Linux 三大发行版的核心区别
linux·ubuntu·debian·linux内核
井队Tell5 小时前
打造高清3D虚拟世界|零基础学习Unity HDRP高清渲染管线(第九天)
学习·3d·unity
渡我白衣5 小时前
C++ 同名全局变量:当符号在链接器中“相遇”
开发语言·c++·人工智能·深度学习·microsoft·语言模型·人机交互
是那盏灯塔6 小时前
【算法】——动态规划之01背包问题
数据结构·c++·算法·动态规划
FserSuN6 小时前
Mem0:构建具有可扩展长期记忆的生产级AI代理 - 论文学习总结1
人工智能·学习
im_AMBER6 小时前
Leetcode 41
笔记·学习·算法·leetcode
Wang's Blog7 小时前
Linux小课堂: NGINX反向代理服务器配置与实践
linux·运维·nginx