先记含义:P申请一个许可,没有许可就等待;V增加一个许可,必要时唤醒一名等待者。 信号量既可数可用资源,也可数已经发生、尚未被使用的事件通知。
1. 为什么需要它
进程或线程交错访问共享数据,可能发生竞争。两线程同时读计数器10,再各加1写回,可能最终只有11。共享打印机、缓冲队列等是临界资源;访问这些资源、必须受保护的那段代码是临界区。P/V可帮助建立两类约束:
| 约束 | 核心问题 | 例子 |
|---|---|---|
| 互斥 | 谁能同时进入 | 一次只允许一个线程修改队列 |
| 同步 | 谁必须先完成 | 数据准备完成后才能处理 |
同一个系统常同时需要两者。PV不能替代业务状态校验、数据库事务或完整的分布式一致性协议。
2. 信号量和原语
信号量通常记S,可以理解为许可计数,并配有等待队列。初值由"起点有几个许可"决定。初始5台可用打印机设S=5;初始空闲的互斥许可设mutex=1;某一次事件尚未完成设done=0。
P也常称wait/down,V也常称signal/up;POSIX接口常见sem_wait、sem_post。重点是语义,而非词源。
P/V对同一信号量的关键计数、判断、入队/唤醒更新需原子地协调,不能用两段未经保护的普通加减替代。原子不意味着等待时占着CPU;记录型阻塞信号量在等待时让出CPU。持锁的业务代码仍可能被抢占,互斥约束的是其他遵守同一锁协议的访问者能否进入临界区。
3. 考试数值计算:统一看题目模型
下文采用教材常见"记录型、P先减1"的模型:
P(S):
S = S - 1
若 S < 0:
当前进程加入S的等待队列并阻塞
V(S):
S = S + 1
若 S <= 0:
唤醒S等待队列中的一个进程
关键共享状态操作由原语原子地协调;此伪代码描述语义,不是可直接用普通整数写出的并发程序。
| S的值 | 该模型下的含义 |
|---|---|
| S>0 | 还有S个未分配许可 |
| S=0 | 没有剩余许可;不能单凭0判断是否有人正处于临界区 |
| S<0 | 没有剩余许可,等待队列中有-S名等待者 |
P执行后等于0,不阻塞;小于0才阻塞。V执行后等于0,仍需唤醒一个等待者。 不要把P的"<0"和V的"<=0"记反。
实际系统接口可以使用非负许可值加独立等待队列,查询值也未必返回负等待人数。因此负数计算只按题设模型使用,不把教材S的表达直接等同某操作系统API。UIC同步课程、sem_getvalue。
4. 手工走一遍:两份资源、四个申请者
初值S=2,甲乙丙丁各申请一份。下列顺序已约定,先不讨论谁优先被唤醒。
| 步骤 | S变化 | 结果 |
|---|---|---|
| 甲P | 2→1 | 获得许可 |
| 乙P | 1→0 | 获得许可,仍可继续 |
| 丙P | 0→-1 | 阻塞 |
| 丁P | -1→-2 | 阻塞 |
| 甲V | -2→-1 | 唤醒一名等待者,假定丙 |
| 乙V | -1→0 | 再唤醒丁 |
| 丙使用完成后V | 0→1 | 归还一份许可 |
| 丁使用完成后V | 1→2 | 全部许可归还 |
负数不是"资源数量变成负数",而是该模型把等待人数计入了S。丙被唤醒后,原来阻塞的P完成,不用额外再执行一次P、再减一次。
被唤醒的进程通常阻塞→就绪,获得CPU后才运行;V不保证被唤醒者立即抢占执行。阻塞等待不等于持续检查条件的自旋。
独立小例:初值S=3,7名进程各执行一次P且期间无人V,则S=3-7=-4,有4名等待者。随后有2名持有者各V一次,S=-2,唤醒2名,仍有2名等待者。这里资源总量仍是3。
5. 用P/V实现互斥
初始临界区空闲:
mutex = 1
每个访问者:
P(mutex)
读取计数器
加1
写回计数器
V(mutex)
P放在完整共享数据操作前,V放在完整操作后。只保护最后一次写回,不能消除前面读取旧值造成的竞争。
mutex初值1表示最初能放行一个访问者;初值2会允许两个同时进入,不能提供要求的互斥;初值0且没人能先V,会让首次申请者都等待。所有相关访问者应遵守同一个互斥协议。
常见错误:遗漏V使后续申请者可能一直等待;多执行一次V可能制造额外许可、破坏互斥。不同锁只保护各自协议下的访问,不能靠变量名一样就认为锁是共享的。
6. 用P/V实现先后同步:对应369题
要求A准备一次数据,B随后处理一次:
done = 0
线程A:
准备数据
V(done)
线程B:
P(done)
处理数据
A负责让事件发生,完成后V;B需要等待事件,处理前P。等待者P,完成者V。
-
done初值1:B可能在A尚未准备时直接通过,破坏先后关系。
-
A在准备前先P(done):A会被阻塞;若唯一通知又依赖A完成,系统无法推进。
-
A在准备前就V(done):B可能过早读到未准备的数据。
-
A先完成V、B后执行P:许可被保存,B仍能通过,不会因为"V发生时没人等"就丢掉这次通知。
互斥常由同一访问者P(mutex)/V(mutex)包住临界区;先后同步可以由不同线程对同一信号量进行V和P,不能机械套"一个线程必须在同一信号量上先P再V"。
7. 前驱关系图:分支与汇合
例如要求A完成后B、C才能分别开始;B和C均完成后D才能开始。B与C之间没有先后约束。
B
↗ ↘
A D
↘ ↗
C
一种容易检查的办法是每条依赖边使用一个初值0的信号量:
sAB = sAC = sBD = sCD = 0
A:执行A;V(sAB);V(sAC)
B:P(sAB);执行B;V(sBD)
C:P(sAC);执行C;V(sCD)
D:P(sBD);P(sCD);执行D
D要收齐两个不同前驱的完成信号。不要只等一次就执行D;也不要让B、C竞争同一个只有一份的许可,却误以为两者都已获通知。边信号量方案便于保证每条依赖,未必是所有题目的最少信号量方案。
8. 生产者消费者:互斥与同步同时存在
缓冲区容量N、初始已有m个产品:
empty = N - m
full = m
mutex = 1
容量不是mutex的初值;mutex保护修改队列这一段代码,一次仍只允许一个访问者。empty/full表示能否进行业务,mutex表示谁能修改共享结构。
| 信号量 | P的含义 | V的含义 |
|---|---|---|
| empty | 申请一个空槽 | 通知腾出一个空槽 |
| full | 申请一个产品 | 通知增加一个产品 |
| mutex | 进入修改队列的临界区 | 退出临界区 |
生产者反复:
在临界区外准备产品
P(empty)
P(mutex)
放入产品并更新队列
V(mutex)
V(full)
消费者反复:
P(full)
P(mutex)
取出产品并更新队列
V(mutex)
V(empty)
在临界区外处理产品
生产者申请空槽、产生产品;消费者申请产品、释放空槽。所以生产者P(empty)对应消费者V(empty),消费者P(full)对应生产者V(full),它们跨角色协作。
先申请空槽或产品,再申请mutex。 在此典型无其他隐藏锁的模型中,操作结束后的两条V可存在其他安全先后变体;不要把所有V顺序互换一律判为死锁。OSTEP信号量章节。
9. 为什么案例会死锁
案例容量3,起初为空,所以初值empty3/full0/mutex1。题目随后给的是缓冲区已满的时刻,不能继续把初始empty3套过去。
满时没有可用空槽。错误生产者先P(mutex)拿锁,再P(empty)等待空槽;消费者要先获得mutex才能取出产品、腾出空槽。
生产者持mutex → 等空槽
空槽要消费者取出产品才产生
消费者要取出产品 → 等mutex
没有其他释放路径,两者相互等待。阻塞的生产者不会自动归还它之前拿到的mutex;P(empty)失败也不是自动回滚之前所有P。
修正申请顺序后,满时生产者会先在P(empty)等待,尚未拿mutex,消费者就能进入取走产品。空时消费者先P(full)等待、不持mutex,生产者也能进入放产品。
10. 相关题型与边界
| 题型 | 检查方法 |
|---|---|
| 求初值 | 问起点能立即发出几个许可:资源N,空闲互斥1,未完成事件0 |
| 判断阻塞/唤醒 | 按题设记录型模型逐步加减,P后<0阻塞,V后<=0唤醒 |
| 求等待人数 | 记录型S<0时为-S,不能当负资源数 |
| 求最多并发数 | N份同类许可、每人占一份,可有N名占用者;临界区mutex1只放行1名 |
| 填P/V位置 | 先识别互斥区,再识别前驱事件;不要一见P就认为是加互斥锁 |
| 判断死锁 | 列每个进程持有什么、等什么,以及谁能解除等待 |
| 判断状态 | P导致等待则运行→阻塞;被V唤醒通常阻塞→就绪 |
| 检查配对 | 互斥用同一许可申请归还;事件和产品许可可以跨线程传递 |
| 检查原子性 | PV内部原子,不代表后续整段业务自动原子;业务仍需正确包住临界区 |
| 判断公平性 | 题目未说明队列策略,不能默认FIFO或断言一定不会饥饿 |
多锁场景若甲先L1后L2、乙先L2后L1,可能循环等待。统一全局申请顺序破坏循环等待;不等于强制夺走资源。
读者写者题再增加"多个读者可并发读,写者需独占"的规则;读者计数自身也可能需要互斥,读/写优先策略可能造成另一方饥饿。哲学家就餐题则常表现为拿着一个资源再等另一个,仍回到资源申请顺序与循环等待。先用本讲义的基本方法列清持有与等待,再处理组合算法。
empty/full在没有预留与等待者计入的稳定状态可以对应实际空槽/产品数量;若题目追踪P成功但放入/取出尚未完成的中间步骤,或采用负等待计数,不能无条件套empty+full=N。
11. 你的复习重点
369:done0,A完成后V,B处理前P。 案例B4:初值3/0/1只是第一步,还要说清持锁等空位形成的循环,并写全生产者/消费者顺序。 下一轮应能不看讲义写出:P和V含义;记录型负数与唤醒;互斥1和事件0的区别;两套有界缓冲区顺序;错误顺序下各方等什么。再用新题和延迟复测验证,不因读过讲义标为已掌握。