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

2024-08-22 08:20

本文主要是介绍【Linux】生产消费模型实践 --- 基于信号量的环形队列,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

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

基于信号量的环形队列

  • 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);
}

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

这篇关于【Linux】生产消费模型实践 --- 基于信号量的环形队列的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1095739

相关文章

0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型的操作流程

《0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeekR1模型的操作流程》DeepSeekR1模型凭借其强大的自然语言处理能力,在未来具有广阔的应用前景,有望在多个领域发... 目录0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型,3步搞定一个应

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

Spring AI Alibaba接入大模型时的依赖问题小结

《SpringAIAlibaba接入大模型时的依赖问题小结》文章介绍了如何在pom.xml文件中配置SpringAIAlibaba依赖,并提供了一个示例pom.xml文件,同时,建议将Maven仓... 目录(一)pom.XML文件:(二)application.yml配置文件(一)pom.xml文件:首

linux进程D状态的解决思路分享

《linux进程D状态的解决思路分享》在Linux系统中,进程在内核模式下等待I/O完成时会进入不间断睡眠状态(D状态),这种状态下,进程无法通过普通方式被杀死,本文通过实验模拟了这种状态,并分析了如... 目录1. 问题描述2. 问题分析3. 实验模拟3.1 使用losetup创建一个卷作为pv的磁盘3.

C++实现封装的顺序表的操作与实践

《C++实现封装的顺序表的操作与实践》在程序设计中,顺序表是一种常见的线性数据结构,通常用于存储具有固定顺序的元素,与链表不同,顺序表中的元素是连续存储的,因此访问速度较快,但插入和删除操作的效率可能... 目录一、顺序表的基本概念二、顺序表类的设计1. 顺序表类的成员变量2. 构造函数和析构函数三、顺序表

python实现简易SSL的项目实践

《python实现简易SSL的项目实践》本文主要介绍了python实现简易SSL的项目实践,包括CA.py、server.py和client.py三个模块,文中通过示例代码介绍的非常详细,对大家的学习... 目录运行环境运行前准备程序实现与流程说明运行截图代码CA.pyclient.pyserver.py参

使用C++实现单链表的操作与实践

《使用C++实现单链表的操作与实践》在程序设计中,链表是一种常见的数据结构,特别是在动态数据管理、频繁插入和删除元素的场景中,链表相比于数组,具有更高的灵活性和高效性,尤其是在需要频繁修改数据结构的应... 目录一、单链表的基本概念二、单链表类的设计1. 节点的定义2. 链表的类定义三、单链表的操作实现四、

如何在本地部署 DeepSeek Janus Pro 文生图大模型

《如何在本地部署DeepSeekJanusPro文生图大模型》DeepSeekJanusPro模型在本地成功部署,支持图片理解和文生图功能,通过Gradio界面进行交互,展示了其强大的多模态处... 目录什么是 Janus Pro1. 安装 conda2. 创建 python 虚拟环境3. 克隆 janus

本地私有化部署DeepSeek模型的详细教程

《本地私有化部署DeepSeek模型的详细教程》DeepSeek模型是一种强大的语言模型,本地私有化部署可以让用户在自己的环境中安全、高效地使用该模型,避免数据传输到外部带来的安全风险,同时也能根据自... 目录一、引言二、环境准备(一)硬件要求(二)软件要求(三)创建虚拟环境三、安装依赖库四、获取 Dee

Linux环境变量&&进程地址空间详解

《Linux环境变量&&进程地址空间详解》本文介绍了Linux环境变量、命令行参数、进程地址空间以及Linux内核进程调度队列的相关知识,环境变量是系统运行环境的参数,命令行参数用于传递给程序的参数,... 目录一、初步认识环境变量1.1常见的环境变量1.2环境变量的基本概念二、命令行参数2.1通过命令编程