Appearance
信号量与同步互斥:同步题的主线是数「许可」(专题总纲)
Intro
同步互斥的模板,大部分人都背过三个:生产者-消费者、读者-写者、哲学家进餐。
然后考场上发下来的题是银行叫号、博物馆参观、三个人植树、两个人用信箱辩论。场景名字全是新的,模板一个都套不上去——很多人就是在这一步卡住的:知道这题考信号量,但不知道该设几个、初值填几。
这个专题要做的就是把这层壳掀了:场景年年换,关系不换。
先说判分:分是按信号量给的
这类题的给分点结构,十几年基本没变过,一道 7~9 分的题拆成这么几档:
- 信号量的定义与初值——设了几个、每个初值多少、含义写没写清;
- 每个进程/线程的结构——P、V 的位置对不对,通常一个进程一档;
- 文字说明——各信号量的含义是否与代码一致。
题面自己也在提醒这件事:多数年份都写着「说明所用信号量及初值的含义」,而这句话不是客套——2011、2013、2015、2025 等年份都为它留了一档独立的分。
所以下笔顺序应该是反的:先把信号量表列全(名字、初值、含义),再写代码。 代码没写完,表还在,那一档分不会掉;反过来,代码誊得再工整、信号量含义一句没写,也拿不到那一档。
信号量只做一件事:数「许可」
把这十几道题的标准答案并排放着看,信号量从头到尾只在做一件事。
信号量是一个带阻塞的计数器,它的值就是「此刻这件事还允许发生几次」。 P 是消耗一次许可(没有就睡过去),V 是产生一次许可(有人在等就叫醒他)。
而许可只有三种来源,对应三种初值:
| 你在数的是什么 | 初值 | 长什么样 |
|---|---|---|
| 临界区的入场券 | 1 | mutex、取号机、铁锹 |
| 「前驱已经完成」的通知 | 通常 0 | full、坑挖好了、叫号了 |
| 资源池里还剩几个 | n | empty、500 个名额、10 个座位 |
13 道真题里没有出现过第四类。 一道题里出现几个信号量,就是你数了几样东西——2013 年的博物馆数了两样(500 个名额、出入口的入场券),2025 年的植树数了四样(坑的名额、坑挖好了、树种好了、铁锹),2009 年那道缓冲区数了四样(缓冲区入场券、空单元、待取的奇数、待取的偶数)。
⚠️ 第二类的初值通常是 0,但不是永远。2015 年双信箱那道题一开始两个信箱里就各有 x、y 封邮件,full 的初值就是 x 和 y——初值是「此刻已经攒下了几个通知」,照着题面数,别背 0。把它写成 0 是那道题的典型失分。
所以不必去记「生产者-消费者需要三个信号量」这种结论。你只要把题面里要数的东西找齐,个数自然就对了。
三步下笔
- 只看题面的名词和动词,列两张关系表:谁和谁抢同一个东西(互斥)、谁必须在谁之后(同步)。这一步不写任何代码。
- 每条关系配一个信号量,初值 = 这件事一开始还允许发生几次。
- 定 P、V 的位置,同时定清楚是谁做。 互斥量的 P 和 V 在同一个进程里,一进一出把临界区夹住;同步量则跨进程——
V由前驱进程在做完之后执行,P由后继进程在开始之前执行。「我知道要设full,但不知道该在生产者那边写 V 还是消费者那边写 V」——判断依据是:谁产生了这个事实,谁 V。
什么时候必须拆成两个信号量
这是真题反复在考的分辨点,也是模板派最容易翻车的地方:
- 2009 年那道题,缓冲区里的奇数和偶数必须用两个信号量。共用一个「有数据了」的信号量,取偶数的进程可能被一个奇数唤醒,醒来发现干不了活。
- 2015 年双信箱辩论,两个信箱各一把锁。共用一把
mutex会按「粒度过粗、无故串行」扣分——两个信箱本来互不相干。(明确写着「最大程度并发」的是 2017 年那道读写冲突题,别记混。) - 2017 年三线程读写共享变量,同一个变量
y要分两把锁——否则两个只读y的线程也被迫互斥了。
只要看一句:如果两个等待者等的不是同一件事,就必须分开数。
排第一的必错点:先同步,后互斥
一个进程要连着执行两个 P 的时候,顺序是死的:先要资源、先等通知,最后才拿锁。 V 的顺序反过来。
这条不是经验之谈。2013 年博物馆那道题按给分点标准,P(empty) 排在 P(mutex) 之前是「进门段」那一档 2 分的判分要点——把顺序写反,第 501 个人就会握着出入口的锁卡在名额上,而里面的人要出来又得先拿这把锁,全场卡死。
记法:抱着锁去等一个只有别人能给的通知,就是死锁。
⚠️ 别把它和 2019 年那道哲学家加碗混成一件事。那道题破的是另一种死锁:碗的数量限流到 min(m, n-1),让「所有人各持一根筷子互等另一根」这个环根本凑不出来——破的是循环等待,不是 P 的顺序。两种死锁的成因不同,答题时说错原因照样扣分。
一道题的答卷长什么样
拿 2013 年博物馆那道题(≤500 人、出入口一次一人)走一遍,卷面上真正要写的就这些:
semaphore empty = 500; // 剩余名额,计数信号量
semaphore mutex = 1; // 出入口互斥,二值信号量
参观者 process:
P(empty); // 先要名额 —— 必须在 P(mutex) 之前
P(mutex);
进门;
V(mutex); // 门锁立刻放掉,别把参观过程圈进临界区
参观;
P(mutex);
出门;
V(mutex);
V(empty); // 出来了才把名额还回去再补一段文字说明(这是独立的一档分,别省): empty 表示当前还能容纳多少人,初值 500;mutex 保证出入口一次只有一人通过,初值 1。
一共就这么长。信号量表 + 每个进程的 P/V 骨架 + 一段含义说明——三样齐了,这一类题的分就基本到手了。时间不够时先写前两样,代码里的业务动作可以只写一个动词。
再看一个有跨进程同步的(2025 年植树那道)
上面那道题只有资源池和互斥,没演示到最让人卡壳的「谁 V 谁 P」。植树这道正好有两条跨进程的通知:
semaphore pit_quota = 3; // 还允许挖几个没人处理的坑
semaphore pit_ready = 0; // 甲 → 乙:坑挖好了
semaphore tree_ready = 0; // 乙 → 丙:树种好了
semaphore shovel = 1; // 甲乙共用的铁锹(水桶只有丙用,不设信号量)
甲: P(pit_quota); P(shovel); 挖坑; V(shovel); V(pit_ready);
乙: P(pit_ready); P(shovel); 放苗填土; V(shovel); V(pit_quota); V(tree_ready);
丙: P(tree_ready); 浇水;对着这三行看那条规则:pit_ready 由甲 V、由乙 P——甲是"坑挖好了"这个事实的制造者, 所以由甲 V;tree_ready 同理由乙 V、丙 P。而 shovel 是互斥量,P 和 V 在同一个进程内成对出现。
还有两个这道题的分辨点:pit_quota 初值是 3 不是 4(题面是「坑数 < 3」); V(pit_quota) 写在乙那里——坑被处理掉了,名额才还回去,不是甲挖完就还。
真题的三种形态
13 道真题按下笔套路分三组。认出自己拿到的是哪一组,比记住任何一个模板都重要。
第一组 · 数许可(9 道)
场景各异,套路完全一致:找关系 → 设信号量 → 定 P/V 位置。
| 年份·题号 | 场景 | 数的是什么 | 这道题的分辨点 |
|---|---|---|---|
| 2009·45 | 缓冲区按奇偶分流取数 | 入场券 / 空单元 / 待取奇 / 待取偶 | 奇偶必须分成两个信号量 |
| 2011·45 | 银行取号与叫号 | 座位 / 取号机 / 等待顾客 / 叫号事件 | 顾客与营业员是双向同步,两个方向各一个 |
| 2013·45 | 博物馆限流参观 | 500 个名额 / 出入口 | 计数信号量与二值信号量并存,P 的顺序定生死 |
| 2014·47 | 消费者要连续取 10 件 | 三件套 + 消费者之间的串行锁 | 「连续取 10 次」要整体包进一对 P/V |
| 2015·45 | 两人用信箱辩论 | 两套三件套,共 6 个 | 每人既是消费者又是生产者;两个信箱不共用锁 |
| 2017·46 | 三线程读写共享变量 | 最小锁集 | 读读不冲突;同一变量按冲突对分两把锁 |
| 2019·43 | 哲学家 + 限量的碗 | n 根筷子 + 碗的限流 | 限流到 min(m, n-1) 破坏循环等待 |
| 2024·46 | 极简同步 / 极简互斥 | 只需要 1 个 | 考的是哪些信号量不需要 |
| 2025·45 | 挖坑—种树—浇水三级流水 | 坑的名额 / 两个方向的通知 / 铁锹 | 只有一个人用的资源(水桶)不设信号量 |
最后两道值得单独留意:它们问的是「最少用几个信号量」。多设一个不会让程序出错,但会丢分——能少设的地方少设,本身就是考点。
第二组 · 前驱图(2 道)
题面给一张操作之间的先后关系图,让你用 P/V 实现。这一组是全部 13 道里最规则的:
一条边一个信号量,初值全 0。前驱做完就 V,后继开始前 P。有几条边就有几个信号量。
上面这张图有 4 条边,就是 4 个初值为 0 的信号量;C 开始前要连 P 两次(等 A、等 B),E 同理。画对图基本就等于做完了。
但 2022 年那道题在这个规则上加了一层:6 个操作分在两个线程里,5 条边中只有 2 条跨线程。线程内部的先后由代码书写顺序天然保证,根本不需要信号量——所以答案只要 2 个。题面并没有写「尽可能少」,但通行答案就是 2 个;多设的那几个边信号量属于没看出「同线程内已经有序」,会丢分。
第三组 · 许可本身凭什么可靠(2 道)
这一组反过来问:P、V 自己凭什么是原子的。
- 2021·45:
wait()里「判断 S 的值」和「S 减 1」是两步,并发会出竞态。用开关中断保证原子性是对的,但关了中断再进忙等循环,别人永远没机会signal,直接死锁——循环体内必须留出一个「开中断;关中断」的窗口。另外,开关中断是特权指令,用户态执行会触发特权违例。 - 2023·45:
swap自旋锁的两处 bug——进入区的if应该是while(一次没抢到还得接着转)、退出区应该把lock置FALSE而不是TRUE;以及「用三条普通语句拼出来的newSwap不能代替硬件原子指令」,论证要靠举一个线程交错的时序反例。
识别信号很清楚:题面给的是一段伪代码让你挑错或论证,而不是一个场景让你设计。 碰到这种题,别急着往生产者-消费者上套——它压根不在问你怎么设信号量。
知道什么时候不该套模板,和会套模板同样重要。
交卷前扫一眼
先列信号量表(名字·初值·含义)· 两个 P 先通知后互斥 · 临界区只放必须互斥的那几行 · 只有一个进程用的资源不设信号量
配套内容
逐题精讲(建设中,将按上面三组展开)——真题作答与 AI 判分入口见站内大题专题。
基础没打牢的,先回这几篇:
考纲要求、但大题里没有正面考过的(选择题会考,别漏):