Skip to content

信号量与同步互斥:同步题的主线是数「许可」(专题总纲)

Intro

同步互斥的模板,大部分人都背过三个:生产者-消费者、读者-写者、哲学家进餐。

然后考场上发下来的题是银行叫号博物馆参观三个人植树两个人用信箱辩论。场景名字全是新的,模板一个都套不上去——很多人就是在这一步卡住的:知道这题考信号量,但不知道该设几个、初值填几。

这个专题要做的就是把这层壳掀了:场景年年换,关系不换。

先说判分:分是按信号量给的

这类题的给分点结构,十几年基本没变过,一道 7~9 分的题拆成这么几档:

  • 信号量的定义与初值——设了几个、每个初值多少、含义写没写清;
  • 每个进程/线程的结构——P、V 的位置对不对,通常一个进程一档;
  • 文字说明——各信号量的含义是否与代码一致。

题面自己也在提醒这件事:多数年份都写着「说明所用信号量及初值的含义」,而这句话不是客套——2011、2013、2015、2025 等年份都为它留了一档独立的分。

所以下笔顺序应该是反的:先把信号量表列全(名字、初值、含义),再写代码。 代码没写完,表还在,那一档分不会掉;反过来,代码誊得再工整、信号量含义一句没写,也拿不到那一档。

信号量只做一件事:数「许可」

把这十几道题的标准答案并排放着看,信号量从头到尾只在做一件事。

信号量是一个带阻塞的计数器,它的值就是「此刻这件事还允许发生几次」。 P 是消耗一次许可(没有就睡过去),V 是产生一次许可(有人在等就叫醒他)。

许可只有三种来源,对应三种初值:

你在数的是什么初值长什么样
临界区的入场券1mutex、取号机、铁锹
「前驱已经完成」的通知通常 0full、坑挖好了、叫号了
资源池里还剩几个nempty、500 个名额、10 个座位

13 道真题里没有出现过第四类。 一道题里出现几个信号量,就是你数了几样东西——2013 年的博物馆数了两样(500 个名额、出入口的入场券),2025 年的植树数了四样(坑的名额、坑挖好了、树种好了、铁锹),2009 年那道缓冲区数了四样(缓冲区入场券、空单元、待取的奇数、待取的偶数)。

⚠️ 第二类的初值通常是 0,但不是永远。2015 年双信箱那道题一开始两个信箱里就各有 x、y 封邮件,full 的初值就是 x 和 y——初值是「此刻已经攒下了几个通知」,照着题面数,别背 0。把它写成 0 是那道题的典型失分。

所以不必去记「生产者-消费者需要三个信号量」这种结论。你只要把题面里要数的东西找齐,个数自然就对了。

三步下笔

  1. 只看题面的名词和动词,列两张关系表:谁和谁抢同一个东西(互斥)、谁必须在谁之后(同步)。这一步不写任何代码。
  2. 每条关系配一个信号量,初值 = 这件事一开始还允许发生几次。
  3. 定 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·45wait() 里「判断 S 的值」和「S 减 1」是两步,并发会出竞态。用开关中断保证原子性是对的,但关了中断再进忙等循环,别人永远没机会 signal,直接死锁——循环体内必须留出一个「开中断;关中断」的窗口。另外,开关中断是特权指令,用户态执行会触发特权违例。
  • 2023·45swap 自旋锁的两处 bug——进入区的 if 应该是 while(一次没抢到还得接着转)、退出区应该把 lockFALSE 而不是 TRUE;以及「用三条普通语句拼出来的 newSwap 不能代替硬件原子指令」,论证要靠举一个线程交错的时序反例。

识别信号很清楚:题面给的是一段伪代码让你挑错或论证,而不是一个场景让你设计。 碰到这种题,别急着往生产者-消费者上套——它压根不在问你怎么设信号量。

知道什么时候不该套模板,和会套模板同样重要。

交卷前扫一眼

先列信号量表(名字·初值·含义)· 两个 P 先通知后互斥 · 临界区只放必须互斥的那几行 · 只有一个进程用的资源不设信号量

配套内容

逐题精讲(建设中,将按上面三组展开)——真题作答与 AI 判分入口见站内大题专题

基础没打牢的,先回这几篇:

考纲要求、但大题里没有正面考过的(选择题会考,别漏):

真题练习