Linux 信号量,环形队列模拟生产者消费者模型 📅 发布时间:2026/7/12 14:58:03 👁️ 浏览次数: 1.信号量信号量本质上就是一个受原子操作保护的计数器。1.1 故事1在日常生活中我们想要去看电影就需要先买票也就是相当于预定一个影院里面的座位加入有100个座位我们就相当于预定了100个座位中的一个之后电影院里面可以预定的座位就变成了99。所以买票也就是对临界资源的预定需要访问临界资源的时候就需要先预定。在影院的后台程序中肯定会有一个类似计数器的东西来实时查看电影院票数的多少当等于0时也就不让别人预定了。1.2 原理在上面的故事中一个一个的座位就相当于临界资源线程或者进程需要访问这一个一个的理解资源就需要先申请当申请成功了计数器就会--对于这个计数器来讲每当有一个进程申请资源那么就让计数器- -当计数器减到0的时候此时进程的执行流再来进行申请对不起没有资源给你使用了同时也只意味着你只可以访问你自己申请到的这一块理解资源不可以访问他人的。所以说进程申请计数器成功代表进程具有访问资源的权限了。进程申请了计数器资源但是此刻我进程却并没有立即去访问资源所以申请计数器资源是对资源的预定机制。计数器的存在可以有效保证进入共享资源执行流的数量所以每一个执行流想要访问共享资源的一部分的时候不是直接去访问而是先申请计数器资源就如同看电影先买票一样的道理程序员把这个“计数器”叫做信号量1.3 故事2如果有一天有一位电影院老板将电影院的放映厅改造改造成只有一个人的座位即整场电影只允许有一个人进行观看此时电影票的计数器就为1所以说众生芸芸如果想要去这个老板的电影院看这场电影最终只能有一个人可以抢到票只能有一个人能够进电影的放映厅看电影1.4 原理所以类似于只能有一个人看电影的这种场景即只能有一个执行流访问临界资源的场景我们称作互斥我们把值只能为01的两态的计数器叫做二元信号量本质上就类似于一个锁只有开锁和锁上那么我们凭什么要让计数器为1其实是资源唯一本质就是不要将临界资源分成很多小块了而是当作一个整体整体申请整体释放。1.5 思考那么我们之前不是提到了--操作不是原子的分为了3条汇编语句那么--操作不是线程安全的啊如果一个线程执行到一半被切换走了就会出现两个线程同时访问一个临界资源这是很危险的所以计数器的--操作一定是线程安全的是原子的。在信号量中计数器的操作称为P操作--操作称为V操作。1.6 总结信号量本质是一把计数器PV操作原子的执行流申请资源必须先申请信号量资源得到信号量后才能访问临界资源信号量值如果是01两态的那么就是二元信号量就是互斥功能申请信号量的本质就是对临界资源的预定机制那么这个时候就有一个疑问了信号量凭什么也是进程间通信的一种进程间通信的定义是可以让两个或多个进程能够互相交换信息协调动作的机制。进程间通信的前提是要让不同的进程看到同一份资源信号量通过被多个线程看到来实现线程间的协调运作。没有它进程A不知道能不能写进程B不知道有没有数据。有了它两个进程才能有序工作。所以信号量是进程间通信的一种它是用来“同步与互斥”的通信是控制并发访问的通信桥梁。1.7 信号量相关接口1.7.1 sem_init使用sem_t类型定义的变量就是一个信号量初始化信号量使用sem_init。第一个参数是信号量的地址第二个参数pshared如果传参0那么表示线程间共享如果传参非零那么表示进程间共享第三个参数是信号量的初始值信号量是一把计数器你想让这把计数器从多少开始那么就在第三个参数传参多少计数器没有负数的概念所以你可以选择传参0或者传参大于0的数。如果第三个参数传参你选择传参0那么进行P操作申请信号量此时没有信号量可以被申请了所以信号量会申请失败后续进行了V操作之后释放了信号量那么此时就有信号量了可以P操作申请信号量成功可见只有当信号量这把计数器大于0才能申请信号量成功1.7.2 sem_destory那么当使用完成信号量之后确保后续不使用信号量了那么使用sem_destroy接口传入信号量的地址即可释放销毁信号量1.7.3 sem_wait如果你想要执行P操作计数器- -即申请一个信号量那么就应该使用sem_wait接口传入信号量的地址即可进行申请信号量如果信号量的计数此时为0那么代表此时没有信号量可以被申请了所以P操作信号量会申请失败后续进行了V操作之后释放了信号量那么此时就有信号量了可以P操作申请信号量成功可见只有当信号量这把计数器大于0才能申请信号量成功1.7.4 sem_post如果你想要执行V操作计数器那么使用sem_post传入信号量的地址即可释放信号量2. 环形队列模拟生产者消费者模型环形队列是一个环形的队列那么我们可以使用一个tail作为环形队列的尾部tail的作用是指向环形队列尾部的格子一个head作为环形队列的头部那么head的作用是指向要放数据的格子最开始的时候我们让tail和head指向一个格子表示环形队列为空即对应上图中的最左边红色即代表格子中已经放了数据白色代表格子中没有数据那么我们先放一个数据那么head向后移tail不动即上图中的中间的环形队列对应的情况接下来我们向环形队列的格子中持续放数据那么head就持续的向后移动直到head和tail重合此时对应上图中的右边的环形队列对应的情况所以此时环形队列为满喽那么问题也就来了什么问题呢最初环形队列为空的时候head和tail重合现在环形队列为满那么head和tail也重合了所以我们就无法区分环形队列究竟是满了还是没有满如果满了继续向格子中放数据那么就会覆盖原有格子中已经存在的数据所以上面设计方法是不可行的因为无法判断环形队列究竟是空还是满那么我们该如何做呢方案一采用一个计数器记录当前有多少个格子已经放了数据和总的格子数目进行比对如果相等那么则说明环形队列满了否则则说明环形队列没有满方案二空一个格子原理如下首先我们判断环形队列的代价是浪费一个空间所以也就注定了会有一个格子始终是不存放数据的那么开始为空的时候我们让head和tail指向同一个位置即对应上图的最左边接下来我们开始放一个数据那么head位置放数据之后head向后移动一个格子那么对应上图中间的情况那么为满的情况是head的下一个位置是tail所以接下来我们开始持续放数据每次放数据前都要判断head的下一个位置是是否是tail如果不是那么说明此时环形队列还没有满所以就向格子中放数据于是格子就持续被放数据直到head的下一个位置是tail那么此时代表环形队列在浪费一个格子的情况下已经被放满了即对应上图的最右侧的情况那么如何出数据呢如下那么出数据的时候要时刻判断tail是否等于head如果不等于那么说明此时环形队列不为空可以出格子中的数据所以此时从最左侧的情况开始从格子中出数据判断tail是否等于head不等于所以出格子中的数据此时将tail位置对应格子的数据弹出然后tail向后走一个格子此时对应上图的中间的情况那么继续出格子中的数据判断tail是否等于head不等于那么就可以持续将tail位置对应格子的数据弹出然后tail向后走一个格子直到tail等于head那么说明此时环形队列为空所以停止出格子中的数据所以tail等于head代表环形队列为空head的下一个为tail代表环形队列在浪费一个格子的情况下为满。2.1 引入信号量上面我们关注的问题就是判断什么时候环形队列为满的什么时候环形队列为空的时候避免为空的数据还去消费数据为满的时候去生产数据覆盖了旧数据。但是实际上面我们只要满足3个条件就可以做到。1.当指向一个位置的会话只能由一个人来访问什么时候生产者和消费者会相遇到就是队列全空和全满的时候全空的时候只能生产者访问全满的时候只能消费者访问。2.消费者不能够超过生产者避免为空的时候还消费数据3.生产者不能把消费者套一个圈避免队列满了还生产数据如果来完成上面的要求呢?(信号量)对于生产者来说关注的就是还有多少格可以生产数据对于消费者来说关注的就是还有多少个数不是为空的对于生产push接口来说当生产者生产了一个数据之后就执行P操作意味着队列中有一个空格有数据了之后为了保证临界资源的安全在多线程的情况下只能有一个线程来对当前位置进行操作的所以需要加锁之后开始对临界资源的操作最后解锁之后执行V操作对可消费的数据。1.疑问为什么对于信息量的PV操作需要放到加锁和解锁之外呢第一点P操作申请信号量以及V操作释放信号量本身就是原子的不需要互斥锁的保护互斥锁的申请与释放保护的代码区域称之为临界区锁的中间不包含P操作V操作可以减少临界区的代码行数进而多线程访问临界区的代码的比重减少由于临界区的代码是被锁保护起来的多个线程要想访问临界区必须先申请锁即对于临界区同一时间只允许一个执行流线程访问所以也就注定了线程必须是串行执行临界区的代码临界区的代码少线程串行执行的时间就会减少进而线程并发的场景增多所以就会提高效率第二点信号量的本质是对资源的预定机制但是预定也需要花费时间的所以P操作V操作也就需要时间没毛病那么如果申请锁之后再P操作申请信号量那么同一时间多个线程只能有一个线程申请到锁然后再申请信号量那么此时就耗费时间了并且此时其它线程由于申请锁失败自然也就无法执行P操作申请信号量信号量可以提前申请因为信号量的本质就是对资源的预定所以如果先执行P操作申请信号量再申请锁那么此时如果有一个线程成功申请到信号量并且成功申请了锁正在执行临界区的代码我们知道执行临界区的代码也是要花费时间的那么于此同时其它线程此时可能也在申请信号量申请信号量也是要花费时间的所以一个线程执行临界区的代码其它线程与此同时可能再申请信号量并发执行代码所以时间消耗也就被极大的减少了当一个线程执行完临界区的代码的时候其它线程此时可能已经申请成功信号量了那么就会直接申请锁所以我们可以在一定程度上让申请成功执行临界区的代码的时间消耗和申请信号量的时间消耗并发消耗而不是串行消耗同样的道理对于释放锁释放信号量也同样使用所以申请锁释放锁要在申请信号量P操作以及释放信号量V操作之间可以提高效率。所以Push是生产者执行的操作为了保证生产者之间的互斥性那么对于生产者对应的锁p_mutex_那么我们就把申请锁释放锁要在申请信号量P操作以及释放信号量V操作之间即可所以Pop是消费者执行的操作为了保证消费者之间的互斥性那么对于消费者对应的锁c_mutex_那么我们就把申请锁释放锁要在申请信号量P操作以及释放信号量V操作之间即可2.2 代码由于这边的代码其实和我们之前写的阻塞队列的差别不多所以直接写多线程版本的了。2.2.1 Task.hpp这个和之前阻塞队列的是一样的。#pragma once #includeiostream #includestring using namespace std; string opers-*/%; enum{ DivZero1, ModZero, Unknown }; class Task { public: Task() {} Task(int x,char op,int y) :data1_(x) ,oper_(op) ,data2_(y) ,result_(0) ,exitcode_() {} void run() { switch(oper_) { case : result_data1_data2_; break; case -: result_data1_-data2_; break; case *: result_data1_*data2_; break; case /: { if(data2_0) exitcode_ModZero; result_data1_/data2_; } break; case %: { if(data2_0) exitcode_DivZero; else result_data1_%data2_; } break; default: exitcode_Unknown; break; } } void operator()() { run(); } string GetResult() { string rto_string(data1_); roper_; rto_string(data2_); r; rto_string(result_); r[code; rto_string(exitcode_); r]; return r; } string GetTask() { string rto_string(data1_); roper_; rto_string(data2_); r?; return r; } ~Task() {} private: int data1_; int data2_; char oper_; int result_; int exitcode_; };2.2.2 main.cpp3个生产者线程不断生成随机的计算任务放入队列3个消费者线程从队列中取出任务并执行。由于生产者线程生产之后会sleep一秒所以每次生产之后会马上被消费掉之后消费者由于没有数据消费会被阻塞。#includectime #includeRingQueue.hpp #includeTask.hpp struct ThreadData { RingQueueTask*rq; string threadname; }; void*Consumer(void*args) { ThreadData*tdstatic_castThreadData*(args); RingQueueTask*rqtd-rq; string nametd-threadname; while(true) { Task t; rq-Pop(t); t();//t.run() coutConsumer get task,task is : t.GetTask()who name result: t.GetResult()endl; } return nullptr; } void*Productor(void*args) { ThreadData*tdstatic_castThreadData*(args); RingQueueTask*rqtd-rq; string nametd-threadname; int lenopers.size(); while(true) { int data1rand()%101; int data2rand()%10; char opopers[rand() % len]; Task t(data1,op,data2); rq-Push(t); coutProductor task done,task is : t.GetResult()who nameendl; sleep(1); } return nullptr; } int main() { srand(time(nullptr)^getpid()); RingQueueTask*bqnew RingQueueTask(50); pthread_t c[3],p[3]; for(int i0;i3;i) { ThreadData*tdnew ThreadData(); td-rqbq; td-threadnameProductor-to_string(i); pthread_create(pi,nullptr,Productor,td); } for(int i0;i3;i) { ThreadData*tdnew ThreadData(); td-rqbq; td-threadnameConsumer-to_string(i); pthread_create(ci,nullptr,Consumer,td); } for(int i0;i3;i) { pthread_join(c[i],nullptr); } for(int i0;i3;i) { pthread_join(p[i],nullptr); } return 0; }2.2.3 RingQueue.hpp所以我们在环形队列RingQueue原有的私有成员变量的基础上添加一把生产者的互斥锁c_mutex_添加一把消费者的互斥锁p_mutex_那么关于锁的申请与释放的接口同样的我们不期望暴露出来所以申请锁封装pthread_mutex_lock为Lock即可释放锁封装pthread_mutex_unlock为Unlock即可那么由于添加了锁所以要在构造函数对锁进行初始化在析构函数对锁进行销毁。push生产者生产数据的接口pop消费者消费数据的接口要注意对于PV操作应该放在加锁解锁的外面对于临界数据的安全则是要放在加锁解锁之间。#pragma once #includeunistd.h #includestdio.h #includeiostream #includevector #includesemaphore.h #includepthread.h using namespace std; const static int defaultcap5; templateclass T class RingQueue { private: void P(sem_tsem) { sem_wait(sem); } void V(sem_tsem) { sem_post(sem); } void Lock(pthread_mutex_t mutex) { pthread_mutex_lock(mutex); } void UnLock(pthread_mutex_t mutex) { pthread_mutex_unlock(mutex); } public: RingQueue(int capdefaultcap) :ringqueue_(cap) ,cap_(cap) ,c_step_(0) ,p_step_(0) { sem_init(c_sem_,0,0); sem_init(p_sem_,0,cap_); pthread_mutex_init(c_mutex_,nullptr); pthread_mutex_init(p_mutex_,nullptr); } void Push(const Tin) { P(p_sem_); Lock(c_mutex_); ringqueue_[p_step_]in; p_step_; p_step_%cap_; UnLock(c_mutex_); V(c_sem_); } void Pop(T*out) { P(c_sem_); Lock(p_mutex_); *outringqueue_[c_step_]; c_step_; c_step_%cap_; UnLock(p_mutex_); V(p_sem_); } ~RingQueue() { sem_destroy(c_sem_); sem_destroy(p_sem_); pthread_mutex_destroy(c_mutex_); pthread_mutex_destroy(p_mutex_); } private: vectorT ringqueue_; int cap_; int c_step_;//消费者下标 int p_step_;//生产者下标 sem_t c_sem_;//消费者关注的数据资源 sem_t p_sem_;//生产者关注的空间资源 pthread_mutex_t c_mutex_; pthread_mutex_t p_mutex_; };
Python flask微信小程序基于Android的社区商品团购系统的设计与实现_ebnbrqe2 目录技术栈选择系统模块划分关键接口设计数据库设计开发里程碑测试策略部署方案开发技术路线源码lw获取/同行可拿货,招校园代理 :文章底部获取博主联系方式!技术栈选择 后端框架采用Python Flask,轻量级且适合快速开发RESTful API。数据库使… 2026/7/11 22:44:52
告别笔记杂乱!Trilium Notes+cpolar,随时随地管好你的知识库 Trilium Notes 是一款适配 Windows、macOS、Linux 多平台的开源笔记工具,核心优势在于树状结构的无限嵌套式笔记管理,既能支撑富文本、Markdown、公式、流程图等多样化编辑需求,还自带全文搜索、笔记加密、版本控制功能,不管是程序… 2026/7/11 13:43:15
从一只龙虾到一支团队:OpenClaw 单 Bot 多 Agent 配置实践 从一只龙虾到一支团队:OpenClaw 单 Bot 多 Agent 配置实践 14 天,从 1 只龙虾裂变成 8 个 Agent 团队。总指挥接收任务→笔杆子写文章→参谋做研究→运营官管日常→进化官写代码→社区官管 Discord→交易官盯股票。724 小时自动协作,老板只需… 2026/5/17 8:33:35
景深失控?构图失焦?Midjourney景深效果完全掌控,8类典型场景适配方案,含--sref与--style raw协同调优秘技 更多请点击: https://kaifayun.com 第一章:景深失控的本质:Midjourney视觉焦点机制深度解构 Midjourney 的图像生成并非基于传统摄影的光学景深模型,而是通过文本提示(prompt)中语义权重与扩散过程中的注意… 2026/7/12 14:56:28
我谈区域偏心率——Gonzalez错了 偏心率的数学定义 禹晶、肖创柏、廖庆敏《数字图像处理(面向新工科的电工电子信息基础课程系列教材)》P312 区域的拟合椭圆看这里。 Rafael Gonzalez的二阶中心矩的表达不说人话。 半长轴和半短轴正比于特征值的根号。Rafael Gonzalez的图11.21和式(… 2026/7/12 14:54:28
AI技术正在改变 人工智能正在改变世界:AI技术的实际应用场景与未来趋势 近年来,人工智能(Artificial Intelligence,简称AI)从一个看似遥远的科技概念,逐渐走进了我们的日常生活和各行各业。从智能手机里的语音助手&#x… 2026/7/12 14:48:26
AutoDock Vina分子对接:新手快速上手的10个实用技巧 AutoDock Vina分子对接:新手快速上手的10个实用技巧 【免费下载链接】AutoDock-Vina AutoDock Vina 项目地址: https://gitcode.com/gh_mirrors/au/AutoDock-Vina AutoDock Vina是一款功能强大且广泛使用的开源分子对接软件,专门用于蛋白质-配体相… 2026/7/12 14:48:26
大唐杯 5G 仿真:从勘站到业务验证的 7 步网络部署全流程拆解 大唐杯5G仿真竞赛:从勘站到业务验证的7步全流程实战解析在5G技术快速落地的今天,掌握网络规划与调测能力已成为通信工程师的核心竞争力。大唐杯全国大学生新一代信息通信技术大赛作为业内权威赛事,其5G仿真竞赛模块通过高度还原真实场景&… 2026/7/12 14:46:25
【Midjourney光影大师级操控指南】:97%用户忽略的5个底层参数与3种物理光模型映射技巧 更多请点击: https://codechina.net 第一章:光影本质与Midjourney渲染管线解构 光在生成式图像模型中并非物理模拟的产物,而是语义驱动的隐式表征。Midjourney 的渲染管线不依赖传统光线追踪引擎,其“光影”由扩散过程中的潜在空… 2026/7/12 14:46:25
Git reset 与 revert 深度对比:5个关键差异与 3 种典型应用场景 Git Reset 与 Revert 深度对比:5个关键差异与3种典型应用场景在团队协作开发中,代码版本管理如同行走钢丝——一步失误可能导致整个项目陷入混乱。作为Git进阶用户,你是否曾在深夜面对错误的提交束手无策?是否在强制推送后收到同事… 2026/7/12 0:01:13
GitHub 学生包申请避坑:5个常见失败原因与开发者工具调试方案 GitHub 学生包申请技术排障指南:5个高频失败场景与开发者工具实战方案第一次尝试申请GitHub学生包时,我盯着屏幕上那个不断转圈的加载动画整整15分钟,最终只等来了一行冰冷的错误提示。这可能是许多开发者共同的经历——明明按照教程操作&… 2026/7/12 0:01:13
冒烟测试用例设计规范:5%-10%覆盖率下的3类核心场景与执行标准 冒烟测试用例设计的黄金法则:5%-10%覆盖率下的精准筛选策略在快节奏的敏捷开发环境中,冒烟测试作为质量保障的第一道防线,其重要性不言而喻。当测试资源有限而时间紧迫时,如何从海量测试用例中精准筛选出那关键的5%-10%࿰… 2026/7/12 0:03:14
Git reset 与 revert 深度对比:5个关键差异与 3 种典型应用场景 Git Reset 与 Revert 深度对比:5个关键差异与3种典型应用场景在团队协作开发中,代码版本管理如同行走钢丝——一步失误可能导致整个项目陷入混乱。作为Git进阶用户,你是否曾在深夜面对错误的提交束手无策?是否在强制推送后收到同事… 2026/7/12 0:01:13
GitHub 学生包申请避坑:5个常见失败原因与开发者工具调试方案 GitHub 学生包申请技术排障指南:5个高频失败场景与开发者工具实战方案第一次尝试申请GitHub学生包时,我盯着屏幕上那个不断转圈的加载动画整整15分钟,最终只等来了一行冰冷的错误提示。这可能是许多开发者共同的经历——明明按照教程操作&… 2026/7/12 0:01:13
冒烟测试用例设计规范:5%-10%覆盖率下的3类核心场景与执行标准 冒烟测试用例设计的黄金法则:5%-10%覆盖率下的精准筛选策略在快节奏的敏捷开发环境中,冒烟测试作为质量保障的第一道防线,其重要性不言而喻。当测试资源有限而时间紧迫时,如何从海量测试用例中精准筛选出那关键的5%-10%࿰… 2026/7/12 0:03:14