当前位置: 首页 > news >正文

死磕信号量实现读者-写者:我被自己写的代码坑惨了

目录

一开始:我看到题,想先不看答案解决“经典问题”

第一回合:“完美”避开死锁,却撞上了死锁

第二回合:死锁修好了,又掉进了“并发度”的坑

第三回合:病急乱投医,想用“关中断”当外挂

结语


最近学习并发编程,感觉自己对信号量和 PV 操作已经拿捏了。正好碰到经典的“读者-写者问题”,结果这一做不要紧,硬是把自己绕进去,反复推翻了好几次,最后才搞明白到底坑在哪。

一开始:我看到题,想先不看答案解决“经典问题”

我当时并没有在操作系统中接触到用到“计数器“的题目,我想,信号量既然表示可用资源的数量,那么拿来统计有多少个读者在读岂不正好?进入时,若该进程为第一个读者,我就申请临界资源使用权;退出时,若是最后一个读者,我就归还临界资源使用权。

我第一版代码是这么写的:

semaphore sWrite = 1; // 写者锁 semaphore sRead = 0; // 读者计数器 Reader() { // 进入区 if (sRead == 0) { P(sWrite); } V(sRead); // 读临界区 ... // 退出区 if (sRead == 0) { V(sWrite); } // 先判断,再减1 P(sRead); } Writer() { P(sWrite); // 写临界区 ... V(sWrite); }

写完之后我还挺得意,觉得逻辑非常完美。但是我交给老师一看,老师开始帮我推演“退出区”的代码时,我当场人就傻了。

第一回合:“完美”避开死锁,却撞上了死锁

我的退出区写的是:先if(sRead == 0)P(sRead)。这就意味着,只要当前同时有读者在读,sRead肯定大于0,这个if判断就永远为假!那个V(sWrite)压根就执行不到。

这意味着什么?意味着第一个进来的读者拿走了写锁,后面无论多少读者进进出出,写锁再也没人归还了!那写者不就直接被饿死(死锁)了吗?我当时真的是拍大腿,代码确实看着挺对,一跑逻辑全是坑。

第二回合:死锁修好了,又掉进了“并发度”的坑

赶紧改呗!把退出区的顺序调换一下,改成“先减1,再判断”,这样最后一个读者走的时候,sRead变 0 了,就正好能把写锁还回去。

// 退出区(调整了顺序:先减1,再判断) P(sRead); // 先减1 if (sRead == 0) { V(sWrite); } // 再判断是否为0

改完之后我长舒一口气,觉得终于把死锁解决了。我再次兴致勃勃的交给老师看改良版的,结果我又往前一看“进入区”的逻辑:

if(sRead == 0) { P(sWrite); }

问题又来了。这俩操作根本就不是原子的!假设两个读者同时到达,他俩都在一瞬间看到sRead == 0,然后都去抢这个P(sWrite)锁。最后肯定会有一个读者抢到锁进去了,另一个被死死卡在外面等着。等第一个读完释放了,第二个才能进。

这哪里还是并发读?这简直就是强制大家串行排队啊!死锁是没了,但是并发度直接归零。我终于悟了,解决死锁只是及格线,不损失并发性能才是真本事。

第三回合:病急乱投医,想用“关中断”当外挂

当时我脑子一热,既然判断和加锁这俩动作没法一气呵成,那我动大招吧,把它俩包在“关中断”里面,看你怎么插队!

还好我没真这么写代码,而是直接把想法告诉了老师,发现这想法离谱到家了:

  1. 多核CPU直接无效:你关了自己这个CPU核心的中断,别的核心上的进程照样抢资源啊,根本无法保证全局原子性。

  2. 用户态根本不让用:关中断是操作系统的底层特权指令。你要是在写的用户程序代码里搞个CLI,程序直接报“非法指令”崩给你看。

  3. 副作用大到爆炸:如果真让你关了中断,系统时钟、磁盘读写、网卡中断全停了,只要几毫秒,你自己的系统就先干宕机了。

所以关中断这种操作系统内核的专属手术刀,根本就不是我们写用户态程序该拿的武器。

终极顿悟:老老实实回到教科书

兜了一大圈,我真的服气了。最后老老实实按照正统解法,引入一把mutex锁,老老实实地在锁保护下访问Rcount

int Rcount = 0; semaphore mutex = 1; // 保护 Rcount semaphore sWrite = 1; // 写者锁 Reader() { P(mutex); if (Rcount == 0) { P(sWrite); } Rcount++; V(mutex); // 读临界区 ... P(mutex); Rcount--; if (Rcount == 0) { V(sWrite); } V(mutex); }

这层mutex锁的精髓就在于:它把“判断if”和“修改Rcount”牢牢捆绑在了一起。在锁的保护下,哪怕是最普通的if也变得绝对安全。

结语

这次折腾虽然耽误了不少时间,但我个人觉得非常值。它让我彻底想明白了两件事:

  1. 并发编程别靠直觉,代码写得“感觉没问题”和真正“原子安全”完全是两码事。判断和行动如果不绑定,必出错。

  2. 死锁只是起步,真正的考验在于你怎么把多线程的并发性能最大化。光顾着防死锁,最后搞出个串行执行,那不仅没优化,反而添乱。

这一波从“自以为是”到“彻底清醒”的过程,确实挺酸爽的,希望对你也有一点点帮助!

http://www.cnnetsun.cn/news/3093392.html

相关文章:

  • 出口工控硬件选型干货:工业 DC-DC/AC-DC 模块电源三点筛选标准丨国产化丨直流电源模块
  • 哈佛等联合研究团队揭开视频生成模型的致命盲区
  • 《Windows Go gRPC 端口占用 bind 报错完整解决方案|Kratos 微服务优雅停机保姆级教程》
  • 3分钟从B站视频到文字稿:bili2text终极指南
  • iSpaRo 2025|月球基地布线,机器人“胳膊不够长”怎么办?
  • 《传世无双》2026年7月最新官网下载:九大元神组合与实战攻略
  • 【JAVA毕设源码分享】基于springboot基于协同过滤课程推荐的线上安全教育平台的设计与实现(程序+文档+代码讲解+一条龙定制)
  • 使用74HC165与ARM Cortex-M4实现高效并行转串行输入设计
  • 后端资源池化:何时用?怎么用?
  • 基于单片机的工件位置控制系统设计
  • AI账号管理与数据备份的实战解决方案
  • 安装登录5分钟
  • go: Handshaking Pattern
  • 看见旋律 - WinUI3 实现音乐监听:47 种漂亮的数学线条形态
  • 实战指南:如何用changedetection.io构建企业级网站变更监控系统
  • 遗传算法实操调参与收敛性诊断实战指南
  • AI 辅助:后端架构选型取舍:没有银弹,只有约束条件
  • 系统调用全路径拆解:从用户态 read(fd) 到内核驱动的上下文切换代价与字符设备实战
  • 3D渲染新范式:从画面像素到全域实景空间 像素流实时建模 新一代视频孪生图形架构
  • AI 辅助:Service Mesh 落地经验:流量治理不是先把边车塞满
  • GitOps 发布实践:声明式配置也需要回滚纪律
  • AI浪潮下普通人焦虑何解?花叔、“五道口纳什”等UP主分享学习路径
  • 企业级检索增强 后端集成:Java 服务如何管理知识库版本
  • PPTist:8个专业模板+完整功能,打造浏览器中的PowerPoint替代方案
  • 工程化工程师的炼丹日常:深夜调参也要守住边界
  • 中餐厅摆台-点击下一步一次显示骨碟碗勺并显示文字 距离
  • STM32寄存器开发练习(一):GPIO-从最原始的代码到规范写法
  • 从推荐系统到大模型:算法工程师的转型实战指南
  • 机械设计公差与配合实战指南:从核心原理到图纸标注
  • 零代码设计小米穿戴表盘:Mi-Create让创意触手可及