【Linux】生产消费模型实践 --- 基于信号量的环形队列

news2024/11/25 12:23:14

在这里插入图片描述

你送出去的每颗糖都去了该去的地方,
其实地球是圆的,
你做的好事终会回到你身上。
--- 何炅 ---

基于信号量的环形队列

  • 1 信号量
  • 2 框架构建
  • 3 代码实现
  • 4 测试运行

1 信号量

信号量本质是一个计数器,可以在初始化时对设置资源数量,进程 / 线程 可以获取信号量来对资源进行操作和结束操作可以释放信号量!
用于多进程 / 多线程 对共享数据对象的读取,它和管道有所不同,它不以传送数据为主要目的,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻只有一个进程独享。 在资源只有一个时就一把互斥锁!

信号量只能进行两种操作获取等待和释放信号,即PV操作:

  1. P(sv):我们将申请获取信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减去一。所以P操作的本质就是让计数器减一,如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行。对应的接口为,使用很简单:
    #include <semaphore.h>
    //阻塞等待获取
    int sem_wait(sem_t *sem);
    //只进行一次获取,非阻塞等待
    int sem_trywait(sem_t *sem);
    //时间片内进行等待,超出就退出阻塞!
    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
    
    
  2. V(sv):我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一。所以V操作本质就是让计数器加一,如果有其他进程 / 线程因等待sv而被挂起,就发送信号让它恢复运行,如果没有进程 / 线程因等待信号量而挂起,就给他加1。对应接口为:
    #include <semaphore.h>
    //释放获取的信号量
    int sem_post(sem_t *sem);
    

PV操作都是原子的,不用担心线程安全!此外信号量初始化和销毁的接口是:

  1. 信号量初始化:
    #include <semaphore.h>
    int sem_init(sem_t *sem, int pshared, unsigned int value);
    
    参数分别为:
    • sem_t *sem:传入信号量的地址
    • pshared:传入0值表示线程间共享,传入非零值表示进程间共享。
    • value:信号量的初始值(计数器的初始值)。
  2. 信号量销毁:
    #include <semaphore.h>
    int sem_destroy(sem_t *sem);
    

2 框架构建

  1. 环形队列的成员变量

    • 线性容器vector模拟环形队列
    • 最大容量 int _max_step
    • 消费者位置 _c_step 与 生产者位置 _p_step
    • 两个信号量来表示生产与消费的剩余容量
      sem_t _data_sem : 当前有多少数据
      sem_t _space_sem: 当前剩余空间还有多少
  2. 构造函数初始化

    • 最大容量需要给值初始化
    • 两个初始位置都为 0
    • 信号量初始化 sem_init() 数据为 0 ,空间为 最大容量
  3. Push接口用来加入数据

    • 首先需要申请信号量 P 来对空间信号量进行获取 sem_wait (&sem_t _space_sem)(申请信号量是原子的)
      获取信号量的本质是对资源 –
    • 生产进行插入 , 对应下标向后移动 , 注意不能越界
    • 最后进行释放信号量 V 来对资源信号量进行释放 sem_post()
      释放信号量的本质是对资源 ++
  4. Pop接口用来获取数据

    • 首先需要申请信号量 P 来对资源信号量进行获取 sem_wait (&sem_t _space_sem)(申请信号量是原子的)
      获取信号量的本质是对资源 –
    • 获取队列资源,并进行释放, 对应下标向后移动 , 注意不能越界
    • 最后进行释放信号量 V 来对空间信号量进行释放 sem_post()
      释放信号量的本质是对资源 ++
  5. 多生产多消费改造:多个生产 / 消费线程存在 消费对消费 生产对生产的问题!

    • 信号量保证了单生产单消费中,两个线程可以通过信号量来保证不会出现访问越界 / 访问重叠的问题!
    • 多线程的情况下可能会发生访问同一位置的可能,获取到信号量之后由于中间的处理是临界区,可能会发生线程的切换,就会导致对同一位置进行处理,进而发生问题!
    • 为了保证线程安全,需要两把锁,分别管理生产者和消费者
    • 锁的处理:
      • 获取信号量之后再进行加锁,获取信号量是原子的,先申请信号量可以保证多个线程在获取中进行排队等待。
      • 如果先加锁,就只能使一个线程进入到获取信号量的队列中,效率低(电影院先买票在排队 ,先排队再买票)

6.为什么信号量不加条件判断?:
在环形队列的实现中,没有使用条件变量,像阻塞队列一样进行条件的判断 而是直接来不管三七二十一进行获取信号量,因为信号量本身就是判断条件,信号量是用来描述内部资源的多少的,是原子的!本质是一个计数器 通过预订机制来保证内部资源的合理使用,当信号量的资源数量为1时和锁时等价的!

3 代码实现

#pragma once

#include <vector>
#include <semaphore.h>

const int default_cap = 5;

template <class T>
class RingQueue
{
public:
    RingQueue(int max_cap = default_cap) : _rq(max_cap), _max_cap(max_cap), _p_step(0), _c_step(0)
    {
        // 信号量初始化
        sem_init(&_space_sem, 0, _max_cap);
        sem_init(&_data_sem, 0, 0);
        //锁进行初始化
        pthread_mutex_init(&_c_mtx , nullptr);
        pthread_mutex_init(&_p_mtx , nullptr);
    }
    // 获取信号量
    void P(sem_t &sp)
    {
        sem_wait(&sp);
    }
    // 释放信号量
    void V(sem_t &sp)
    {
        sem_post(&sp);
    }
    // 插入操作
    void Push(const T &t)
    {
        // 获取空间信号量 --
        P(_space_sem);
        //临界区上锁
        pthread_mutex_lock(&_p_mtx );
        _rq[_p_step] = t;
        _p_step++;
        _p_step %= _max_cap;
        //解锁
        pthread_mutex_unlock(&_p_mtx);
        // 释放信号量 ++
        V(_data_sem);
    }
    // 获取操作
    void Pop(T *t)
    {
        // 获取资源信号量
        P(_data_sem);
        pthread_mutex_lock(&_c_mtx);
        *t = _rq[_c_step];
        _c_step++;
        _c_step %= _max_cap;
        pthread_mutex_unlock(&_c_mtx);
        // 释放信号量
        V(_space_sem);
    }
    ~RingQueue()
    {
        // 销毁对应信号量!
        sem_destroy(&_space_sem);
        sem_destroy(&_data_sem);
        //锁进行释放
        pthread_mutex_destroy(&_c_mtx);
        pthread_mutex_destroy(&_p_mtx);
    }

private:
    // 底层线性结构,模拟环形队列
    std::vector<T> _rq;
    // 最大容量
    int _max_cap;
    // 生产者/消费者 下标
    int _p_step;
    int _c_step;
    // 空间/资源 信号量
    sem_t _space_sem;
    sem_t _data_sem;
    // 生产 / 消费 锁
    pthread_mutex_t _p_mtx;
    pthread_mutex_t _c_mtx;

};

4 测试运行

我们来做一些简单测试,我们设计了Task类,用于执行加法操作。它包含两个整型参数_x_y,并提供方法来执行加法并获取结果。通过重载括号运算符,Task对象可以被直接调用以执行计算。此外,类还提供了调试信息和结果输出的功能。

我写了一段代码段用于测试。在该测试中:定义了两个线程函数ConsumerProductor,分别模拟消费者和生产者行为:

  1. Consumer线程不断从环形队列中取出Task对象,执行其操作,并打印消费结果。
  2. Productor线程则持续生成新的Task对象并将其放入队列中,同时打印出生产信息。

主函数main中创建了一个容量为5的RingQueue<Task>实例,并启动了两个线程。pthread_create用于创建线程,pthread_join确保主线程等待子线程执行完毕。通过这种方式,我们验证了环形队列在多线程环境下的线程安全性和功能正确性。

#include <iostream>
#include "RingQueue.hpp"
#include <pthread.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include "Task.hpp"

void *Consumer(void *args)
{
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    srand(time(nullptr) ^ getpid());
    while (true)
    {
        // 不断的进行获取
        Task data ;
        rq->Pop(&data);
        data();
        std::cout << "Consumer 消费者消费 -> " << data.result() << std::endl;

        sleep(1);
    }
}
void *Productor(void *args)
{
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    srand(time(nullptr) ^ getpid());
    while (true)
    {
        // 不断的进行写入
        int num1 = rand() % 10;
        usleep(1000);
        int num2 = rand() % 10;
        Task t(num1 , num2);
        rq->Push(t);
        std::cout << "Productor 生产者生产 -> " << t.debug() << std::endl;

        usleep(10000);
    }
}

int main()
{
    // 环形队列
    RingQueue<Task> rq(5);
    // 使用两个线程来测试
    pthread_t t1, t2;

    pthread_create(&t1, nullptr, Consumer, &rq);
    pthread_create(&t2, nullptr, Productor, &rq);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
}

运行效果:
在这里插入图片描述
很好的完成了任务!!!

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

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

相关文章

数据结构——链式队列和循环队列

目录 引言 队列的定义 队列的分类 1.单链表实现 2.数组实现 队列的功能 队列的声明 1.链式队列 2.循环队列 队列的功能实现 1.队列初始化 (1)链式队列 (2)循环队列 (3)复杂度分析 2.判断队列是否为空 (1)链式队列 (2)循环队列 (3)复杂度分析 3.判断队列是否…

91. UE5 RPG 实现拖拽装配技能以及解除委托的绑定

在上一篇文章里&#xff0c;实现了通过选中技能&#xff0c;然后点击下方的装备技能插槽实现了技能的装配。为了丰富技能装配功能&#xff0c;在这一篇里&#xff0c;我们实现一下通过拖拽技能&#xff0c;实现拖拽功能&#xff0c;我们需要修改两个用户控件&#xff0c;一个就…

鸿蒙内核源码分析(信号生产篇) | 注意结构体的名字和作用.

信号生产 关于信号篇&#xff0c;本只想写一篇&#xff0c;但发现把它想简单了&#xff0c;内容不多&#xff0c;难度极大.整理了好长时间&#xff0c;理解了为何<<深入理解linux内核>>要单独为它开一章&#xff0c;原因有二 信号相关的结构体多&#xff0c;而且…

RTC碰到LXTAL低频晶振停振怎么办?

GD32F303的RTC模块框图如下图所示&#xff0c;RTC时钟源可选择HXTAL/128、LXTAL或IRC40K&#xff0c;一般为了实现更精准的RTC时间&#xff0c;MCU系统均会外挂32.768KHz LXTAL低频晶振&#xff0c;但由于低频晶振负阻抗较大&#xff0c;不容易起振&#xff0c;若外部电路布线、…

vue3 antdv3 去掉Modal的阴影背景,将圆角边框改为直角的显示,看上去不要那么的立体的样式处理。

1、来个没有处理的效果图&#xff1a; 这个有立体的效果&#xff0c;有阴影的效果。 2、要处理一下样式&#xff0c;让这个阴影的效果去掉&#xff1a; 图片的效果不太明显&#xff0c;但是阴影效果确实没有了。 3、代码&#xff1a; /* 去掉遮罩层阴影 */.ant-modal-mask {…

Maven命令传pom或者jar异常

上传命令&#xff1a;mvn deploy:deploy-file -Durlhttp://****&#xff1a;****/repository/chntdrools7741-releases -DrepositoryId**** -DfileD:/tempRepo/org/kie/kie-api-parent/7.69.0.Final/kie-api-parent-7.69.0.Final.pom -DpomFileD:/tempRepo/org/kie/kie-api-par…

三级_网络技术_39_综合题(命令)

一、 如下图所示&#xff0c;某校园网用10Gbps 的POS技术与Internet相连&#xff0c;POS接网的幅格式早SDH。路由协议的选择方案是校园网内部采用OSPF协议&#xff0c;校园网与lntemnet的连接使用静态路由协议。校园网内的路由器R1设为DHCP服务器&#xff0c;可分配的IP地址是…

【22-54】创建者模式(详解五大模式)

目录 一.创建者模式介绍 二.单例设计模式 2.1 单例模式的结构 2.2 单例模式的实现 2.2.1.1 饿汉式-方式1&#xff08;静态变量方式&#xff09; 2.2.1.2 饿汉式-方式2&#xff08;静态代码块方式&#xff09; 2.2.2.1 懒汉式-方式1&#xff08;线程不安全&#xff09; 2…

用手机写一本电子书

第1步、进入Andi.cn网站 第2步、点击登录&#xff0c;注册用户 第3步、点击去创作&#xff0c;进入创作页面 第4步、点击右下角的小笔&#xff0c;写一篇文章 第5步、下翻&#xff0c;点击提交按钮 第6步、再写一篇文章 第7步、点击栏目设计 第8步、进入栏目设计&#xff0c;点…

excel卓越之道笔记

excel快捷键 1.Alt+=一键求和 2.Tab补全函数名称 3.CONCAT可以连选,CONCATENATE只能一个单元格一个单元格点选 4.excel365用不了phonetic函数,但是可以用concat代替 5.textjoin连接标识码,在Arcgis中筛选出所需要素,也是很好用的 6.法1:alt+; 定位可见单元格,复制后只…

Linux入门——01常用命令

0.命令行解释器shell 用户无法直接给操作系统指令&#xff0c;需要经过shell,才能让操作系统明白。如果用户对操作系统非法操作&#xff0c;会有shell保护。shell本身也是一个进程&#xff0c;当然&#xff0c;用户给shell的指令&#xff0c;shell会派生出子进程进行执行&#…

Unity Protobuf3.21.12 GC 问题(反序列化)

背景&#xff1a;Unity接入的是 Google Protobuf 3.21.12 版本&#xff0c;排查下来反序列化过程中的一些GC点&#xff0c;处理了几个严重的&#xff0c;网上也有一些分析&#xff0c;这里就不一一展开&#xff0c;默认读者已经略知一二了。 如果下面有任何问题请评论区留言提…

【Kubernetes中如何对etcd进行备份和还原】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

不同路径

不同路径 思路&#xff1a; 法一&#xff1a;动态规划 const int N 110; class Solution { int dp[N][N];//dp[i][j]&#xff1a;从起点走到 i j的路径个数。 public:int uniquePaths(int m, int n) {for(int i1;i<n;i){dp[1][i]1;} for(int i1;i<m;i) dp[i][1]1;f…

day36.动态规划+重载操作符

动态规划好难啊(ಥ﹏ಥ) 终于搞懂0-1背包问题的二维数组转一维数组优化的问题了。如图所示: 将二维数组转换成一位数组的核心就是&#xff0c;dp[i][j]选取时&#xff0c;他的值只与dp[i-1][j]&#xff0c;也就是上一行有关&#xff0c;所以可以引出使用一维数组代替二维数组…

python 使用宝塔面板在云服务器上搭建 flask

打开宝塔面板到【网站】&#xff0c;选择【python项目】&#xff0c;点【添加python项目】 填上相关信息&#xff1a; 注意&#xff1a;项目端口是你打算在外网用来访问flask的端口号 勾选【放行端口】&#xff0c;并提交 到阿里云里&#xff0c;选择安全组 手动添加放行端口…

datawind可视化查询-其他函数

飞书文档学习链接:https://www.volcengine.com/docs/4726/47275 1. 用户名函数 用户名函数并非 ClickHouse 官方函数,而是与项目用户信息相结合,用于返回当前使用用户的指定信息的函数。 USERNAME()可返回当前用户的用户名,如下所示。该函数也可与其他函数组合使用 2. J…

51 无显式主键时 mysql 增加的 DB_ROW_ID

前言 这里主要是 探讨, 在我们创建了一个 无主键的数据表, 然后 mysql 会为我们增加的这一个 DB_ROW_ID 的相关 新建一个无主键字段的数据表如下 CREATE TABLE implicit_id_table (username varchar(16) DEFAULT NULL,age int(11) DEFAULT NULL ) ENGINEInnoDB DEFAULT CH…

MySQL范围分区分区表

什么是范围分区分区表&#xff1f; 范围分区是一种根据某个列的范围值来分割表数据的分区方式。在范围分区中&#xff0c;每个分区都有自己的范围条件&#xff0c;当插入数据时&#xff0c;MySQL会根据指定的范围条件将数据分配到相应的分区中。这种分区方式可以使得表的数据按…

2024前端面试题-css篇

1.p和div区别 p自带有一定margin-top和margin-bottom属性值&#xff0c;而div两个属性值为0&#xff0c;也便是两个p之间有不一定间距&#xff0c;而div没有。 2.对css盒模型的理解 标准盒模型&#xff1a;content不包括padding、border、margin ie盒模型&#xff1a;conten…