当前位置: 首页 > 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.jsqmd.com/news/1105965/

相关文章:

  • 市县级全域旅游智慧导览电子地图制作实操(三)AI+人工生成全域手绘地图
  • Xinference开源大模型本地部署实战指南
  • 工业级条码扫描模块与PIC32MZ嵌入式方案解析
  • 3分钟掌握Illustrator智能填充:Fillinger脚本让你的设计效率翻倍
  • 网络流量分类技术:从机器学习到硬件优化实践
  • UABEA:重新定义Unity资源编辑的跨平台革命
  • 迅雷网盘在线解析:高速直链下载的方法
  • 目标检测分类部分损失函数:BCE → Focal Loss → VFL → MAL 的演进
  • okbiye 毕业论文 AI 写作实操指南|界面全功能拆解,一站式搞定学位论文撰写
  • UE5快捷键速查
  • 主流VST头显视觉性能对比:Vision Pro、Quest 3与Quest Pro评测
  • 大厂高频面试题:手机号加密存储后,如何快速按尾号查询?
  • AI一周事件 · 2026-W27(6月24日–6月30日)
  • 终极Windows驱动管理指南:DriverStoreExplorer免费释放C盘空间
  • BetterNCM Installer:3步解锁网易云音乐隐藏功能
  • 为了防止题目链接失效,将题目原文复制如下:
  • 基于 epoll 的协程调度器——零基础深入浅出 C++20 协程
  • 7_CSS预处理器Sass
  • Sonnet 5 发布:Prompt 已死,Loop 当
  • Java实现Navicat密码加密解密:AES-256-CBC本地安全存储实战
  • 短效代理适合哪些业务场景?资深玩家实测科普适配场景指南
  • 使用74HC165与ARM Cortex-M4实现高效并行转串行输入设计
  • QuickVina 2深度解析:20倍加速的分子对接性能揭秘
  • IS31FL3731 LED驱动芯片与PIC18F24K50微控制器的嵌入式开发实践
  • 【精通】SmartWriter v2.5:写作平台 CI/CD — 提示词版本管理、A/B 评测与回归验证深度实战
  • Go 进阶必修:90% 的人都没用对的“表驱动法”
  • 关于动态规划【力扣300.最长递增子序列的思考】
  • 给制造以光,让智造有根:中策橡胶卓越智能工厂背后的F5G-A全光力量
  • 华为MetaERP Oracle EBS R12 AP 供应商主数据完整配置指南(架构师实施版)一、前置基础配置(必须先完成,否则供应商无法正常使用)(一)财务选项 Financials Opti
  • 基于树莓派的边缘计算安全网关设计与实现