Appearance
经典同步问题:生产者-消费者、读者-写者、哲学家进餐
概念
三大经典同步问题是操作系统里"把 os/13-semaphore.md 的规则拿来实战"的三道标准题,408 综合大题几乎每年从这三个里出一道:
| 问题 | 共享资源 | 约束 | 难度 |
|---|---|---|---|
| 生产者-消费者 | 大小为 | 缓冲区满不能放、空不能取;放/取要互斥 | 同步 + 互斥混合(最典型) |
| 读者-写者 | 一份数据/文件 | 读读可并行;读写、写写必须互斥 | 需要计数(第一个/最后一个读者) |
| 哲学家进餐 | 5 根筷子(每根同时只能被 1 人拿) | 拿到左右两根才能吃;吃完同时放下 | 多个互斥资源的循环等待(死锁) |
一句话说清它们的共同点:三者都是"用信号量把并发进程之间的次序与独占关系写清楚"——生产者-消费者考"同步+互斥的混合编排",读者-写者考"用计数信号量做条件判断",哲学家考"如何避免循环等待造成的死锁"。
原理
一、生产者-消费者:三个信号量各管一件事
先分清哪是"互斥"、哪是"同步"(这是本题的得分点):
| 信号量 | 类型 | 初值 | 管什么 |
|---|---|---|---|
mutex | 互斥 | 1 | 同一时刻只有一个人能操作缓冲区 |
empty | 同步(资源计数) | 还剩几个空位(= 生产者能放几个) | |
full | 同步(资源计数) | 0 | 当前有几个产品(= 消费者能取几个) |
c
semaphore mutex = 1; /* 互斥访问缓冲区 */
semaphore empty = n; /* 空位数, n 为缓冲区大小 */
semaphore full = 0; /* 产品数 */
/* 生产者 */
while (true) {
生产一个产品;
P(empty); /* ① 先看有没空位 (没有就睡, 不占锁) */
P(mutex); /* ② 再抢锁 */
把产品放入缓冲区;
V(mutex); /* ③ 放锁 */
V(full); /* ④ 通知"多了一个产品" */
}
/* 消费者 */
while (true) {
P(full); /* ① 先看有没产品 */
P(mutex); /* ② 再抢锁 */
从缓冲区取出一个产品;
V(mutex);
V(empty); /* ③ 通知"多了一个空位" */
消费该产品;
}⚠️ 四条必须记住的要点:
P(empty)必须在P(mutex)之前(先同步、后互斥)——否则会"拿着锁等空位",把消费者也堵死 → 死锁(例 2 有完整轨迹)。- 两个
V的顺序无所谓(V只做加法与唤醒,交换不会出错)。 empty与full是"一对互补量": (放一个产品 = 空位少一个、产品多一个)——这条恒等式是自检 PV 写对没有的最快办法。mutex是必需的吗?——缓冲区大小 时可以省掉(见例 1 末尾的分析)。
二、读者-写者:用 count 把"多人共享"变成一个"整体"
核心矛盾:读者之间可以并行(不需要互斥),但"有读者在"这件事对写者来说必须是一把锁——于是需要一个 count 计数器把"所有读者"当成一个整体。
c
semaphore rw = 1; /* 读写互斥锁: 有读者或写者在用, 别人都得等 */
semaphore mutex = 1; /* 保护 count 这个共享变量 */
int count = 0; /* 当前正在读的读者数 */
/* 读者 */
P(mutex);
count++;
if (count == 1) P(rw); /* ★ 第一个读者: 上"读写锁" */
V(mutex);
读数据;
P(mutex);
count--;
if (count == 0) V(rw); /* ★ 最后一个读者: 释放"读写锁" */
V(mutex);
/* 写者 */
P(rw);
写数据;
V(rw);⚠️ 四个得分点:
if (count == 1) P(rw)与if (count == 0) V(rw)是"配对"的——第一个读者上锁、最后一个读者开锁,中间的读者只是count++。count是共享变量,必须用mutex保护——否则两个读者可能同时读到count = 0而都去P(rw)(一个拿到、另一个阻塞)。- 读者优先的代价:写者可能饥饿(只要读者络绎不绝,
count永远不为 0,写者永远等不到rw)。 - 写者优先要额外加"挡板"(见例 3 的方案 B)。
三、哲学家进餐:死锁的教科书现场
约定:5 位哲学家围圆桌,每人左右各一根筷子,共 5 根;进餐需要同时持有左右两根;吃完同时放下。
c
semaphore chopstick[5] = {1, 1, 1, 1, 1}; /* 每根筷子是一个互斥信号量 */
/* 哲学家 i (左 = i, 右 = (i+1) % 5) —— 朴素版, 会死锁 */
while (true) {
思考;
P(chopstick[i]); /* ① 先拿左手 */
P(chopstick[(i + 1) % 5]); /* ② 再拿右手 —— 若拿不到, 就"握着左筷子等" */
进餐;
V(chopstick[i]); /* 吃完两手同时放 */
V(chopstick[(i + 1) % 5]);
}死锁是怎么发生的(必须能描述):5 个人同时执行了第 ① 步,每人手里都有一根筷子,然后全体卡在第 ② 步——每人都"持有一个资源、等待另一个资源",构成一个完整的等待环,谁都不放,永远僵住。
四种解法(全部要能写):
| 解法 | 做法 | 为什么有效 | 代价 |
|---|---|---|---|
| ① 限制人数 | 加一个初值 4 的信号量 num,进餐前 P(num) | 最多 4 人同时争 5 根筷子 → 至少有一人能拿到两根,环不可能闭合 | 并发度略降 |
| ② 奇偶顺序 | 偶数号先拿左、奇数号先拿右 | 相邻两人的"第一根"方向不同,不会形成单向等待环 | 实现稍绕 |
| ③ 一次拿两只(AND 型信号量) | 用 Swait(chop[i], chop[(i+1)%5]) 一次原子地申请两根 | "要么都拿到、要么一个都不占" → 不存在"握着半根等" | 需要 AND 型信号量支持 |
| ④ 加全局互斥 | 拿筷子前先 P(mutex),拿完再 V(mutex) | 同一时刻只有一人在"拿筷子" | 并发度最差(退化为串行拿筷子) |
⚠️ 解法 ③ 的写法(408 常考):"AND 型信号量"要求多个资源"全部拿到才占用、有一个拿不到就全部不占"——这直接消灭了"部分占有"这个死锁条件。若题目不允许 AND 型信号量,就用解法 ② 或 ①。
四、三个问题的共同套路
① 先分清: 几个"互斥"点? 几个"同步"点?
② 互斥 -> 每处一个初值 1 的信号量; 同步 -> 每处一个初值 0 (或资源数) 的信号量
③ 编排: P 序"先同步/资源、后互斥"; V 序随意
④ 自检: 互补量之和是否恒定? 每个信号量 P/V 是否等量? 有没有"持锁等待"?示例
例 1:生产者-消费者的一次合法执行(12 步完整轨迹)
缓冲区大小
;初值 mutex = 1、empty = 3、full = 0。下面是一次合法的交错执行,写出每步之后三个信号量的值。
| 步 | 执行者 | 操作 | mutex | empty | full | 说明 |
|---|---|---|---|---|---|---|
| 1 | 生产者 | P(empty) | 1 | 2 | 0 | 有空位,通过 |
| 2 | 消费者 | P(full) | 1 | 2 | −1 | 没有产品 → 阻塞 |
| 3 | 生产者 | P(mutex) | 0 | 2 | −1 | 拿到缓冲区锁 |
| 4 | 生产者 | 放入产品 | 0 | 2 | −1 | 真正操作缓冲区 |
| 5 | 生产者 | V(mutex) | 1 | 2 | −1 | 放锁 |
| 6 | 生产者 | V(full) | 1 | 2 | 0 | 0 <= 0 → 唤醒消费者 |
| 7 | 消费者 | P(mutex) | 0 | 2 | 0 | 消费者被调度后拿锁 |
| 8 | 生产者 | P(empty) | 0 | 1 | 0 | 还有空位,通过(不碰 mutex) |
| 9 | 消费者 | 取走产品 | 0 | 1 | 0 | 消费者在临界区内 |
| 10 | 生产者 | P(mutex) | −1 | 1 | 0 | 锁在消费者手里 → 阻塞 |
| 11 | 消费者 | V(mutex) | 0 | 1 | 0 | 0 <= 0 → 唤醒生产者 |
| 12 | 生产者 | 放入产品 | 0 | 1 | 0 | 生产者恢复后放入 |
三条核对:
empty + full恒为 3(第 4 步之后: ?不成立!)——⚠️ 注意:full = -1时它不是"产品数",而是"有 1 个消费者在等"。互补恒等式只在"没有进程阻塞时"成立:第 1~5 步区间内 (因为有一个消费者被阻塞,"应到未到"的那 1 个产品被记为负数)。正确说法:"实际产品数" =
full(若full ≥ 0)或0(若full < 0),而 。第 8 步生产者能
P(empty)成功,是因为它没被锁挡住——empty与mutex是两把独立的"门"。第 10 步的
mutex = -1表示"1 个进程在等这把锁"——不是"锁的数量是 −1"(回顾os/13-semaphore.md的三种含义)。
⚠️ 缓冲区大小 mutex 吗?——可以。
理由(完整推理):empty 与 full 恰好互补(初值 empty = 1、full = 0)。生产者必须先通过 P(empty),消费者必须先通过 P(full):
两边永远不可能同时进入操作缓冲区的那段代码 → 互斥自然成立,无需 mutex。
⚠️ 这是 408 的经典判断题:"缓冲区大小为 1 时,互斥信号量可以省略"——对。但
n > 1时绝不能省(多个生产者可以同时P(empty)成功)。
例 2:P 顺序颠倒 → 把系统锁死
把生产者的
P(mutex)提到P(empty)前面,初始状态改成"缓冲区已满"(empty = 0、full = 3)。
错误版本(P(mutex) 在前):
| 步 | 执行者 | 操作 | mutex | empty | full | 结果 |
|---|---|---|---|---|---|---|
| 1 | 生产者 | P(mutex) | 0 | 0 | 3 | 拿到锁 |
| 2 | 消费者 | P(full) | 0 | 0 | 2 | 拿到一个产品名额 |
| 3 | 生产者 | P(empty) | 0 | −1 | 2 | 拿着锁去等空位 → 阻塞 |
| 4 | 消费者 | P(mutex) | −1 | −1 | 2 | 等锁 → 阻塞 |
| 5 | — | — | — | — | — | 双方阻塞 = 死锁 |
死锁的机制:生产者持有
mutex且在等empty;消费者持有full名额(已从full里减掉)且在等mutex——两个人都"握着一样、等着另一样",环闭合了。
正确版本(P(empty) 在前),同一初始状态:
| 步 | 执行者 | 操作 | mutex | empty | full | 结果 |
|---|---|---|---|---|---|---|
| 1 | 生产者 | P(empty) | 1 | −1 | 3 | 没空位 → 阻塞(但没有持锁!) |
| 2 | 消费者 | P(full) | 1 | −1 | 2 | 拿到产品名额 |
| 3 | 消费者 | P(mutex) | 0 | −1 | 2 | 拿到锁 |
| 4 | 消费者 | 取走产品 | 0 | −1 | 2 | 有空位了 |
| 5 | 消费者 | V(mutex) | 1 | −1 | 2 | 放锁 |
| 6 | 消费者 | V(empty) | 1 | 0 | 2 | 0 <= 0 → 唤醒生产者 |
→ 不死锁。差别只有一处:生产者"没空位时是否还攥着锁"。 这就是"先同步、后互斥"这条规则的全部内容。
例 3:读者-写者的状态推演与写者优先方案
A. 读者优先方案的状态推演
时序:R1 进入 → R2 进入 → W 到达 → R1 退出 → R2 退出 → W 进入。
| 时刻 | 事件 | count | rw | mutex | 说明 |
|---|---|---|---|---|---|
| 0 | 初始 | 0 | 1 | 1 | 无读者、无写者 |
| 1 | R1 进入 | 1 | 0 | 1 | 因 count == 1 而 P(rw),把写者挡在门外 |
| 2 | R2 进入 | 2 | 0 | 1 | count != 1,不动 rw → 读者可以不断叠加 |
| 3 | W 到达 | 2 | 0 | 1 | P(rw) 阻塞 → 写者饥饿 |
| 4 | R1 退出 | 1 | 0 | 1 | count != 0,不 V(rw) → 写者继续等 |
| 5 | R2 退出 | 0 | 1 | 1 | count == 0 才 V(rw),唤醒写者 |
| 6 | W 进入 | 0 | 0 | 1 | 写者终于拿到 rw |
⚠️ 三个关键点:
rw只在"第一个读者进来"和"最后一个读者走"时被碰——中间的读者对rw毫无影响。- 写者饥饿的根因:只要
count在 W 等待期间始终 (总有至少一个读者在读),V(rw)就永远不会执行。 mutex的值始终在 0/1 之间(保护count的短暂临界区)。
B. 写者优先方案(加一个"挡板"信号量 w)
c
semaphore rw = 1, mutex = 1, w = 1; /* w: 写者优先挡板 */
int count = 0;
/* 读者 */
P(w); /* ★ 挡板: 有写者在等, 后来的读者就进不来 */
P(mutex); count++; if (count == 1) P(rw); V(mutex);
V(w);
读数据;
P(mutex); count--; if (count == 0) V(rw); V(mutex);
/* 写者 */
P(w); /* ★ 写者先抢挡板 */
P(rw); 写数据; V(rw);
V(w);机制:写者一到达就抢 w——如果此时还有读者在读,写者会在 P(rw) 上阻塞,但它"握着 w",于是后续读者全卡在 P(w) → 读者不再增加,当前读者读完就把 rw 交给写者。
代价:角色互换——读者可能饥饿(写者络绎不绝时,读者永远进不来)。
例 4:哲学家进餐——死锁复现与两种解法对照
用固定调度顺序模拟 5 位哲学家(每人先拿左、再拿右):
| 步 | 动作 | 筷子状态 |
|---|---|---|
| 1 | P0 P(c0) 通过 | c0 被占 |
| 2 | P1 P(c1) 通过 | c1 被占 |
| 3 | P2 P(c2) 通过 | c2 被占 |
| 4 | P3 P(c3) 通过 | c3 被占 |
| 5 | P4 P(c4) 通过 | c4 被占 |
| 6 | 5 人各自 P(右) → 全部阻塞 | 5 根筷子各被 1 人握着,5 人全在等 |
判定:死锁(每根筷子的 value 都是
换成"奇偶顺序"(偶数号先左、奇数号先右)后:
| 度量 | 朴素版(先左后右) | 奇偶顺序版 |
|---|---|---|
| 是否死锁 | 死锁 | 无死锁 |
| 模拟 120 轮的进餐次数 | 0(全体卡死) | 85 |
为什么奇偶顺序有效:P0 先拿 c0、P1 先拿 c1……但如果 P0 与 P1 相争,他们争夺的"第一根"分别是 c0 与 c1(不重叠)——真正会造成"环"的是"所有人按同一方向转圈",奇偶交错把方向拆成一半一半,等待关系不再能首尾相接成环。
⚠️ 严谨提醒:"奇偶顺序不会死锁"是本题的通行结论,但严格证明要说明"资源申请是一种偏序"——考试答题写清"相邻哲学家第一根筷子的方向相反,故不存在循环等待"即可。
例 5:C 实现——生产者-消费者的 12 步轨迹
#include <stdio.h>
/* 三个信号量: 只保留教材关心的"会计语义"(用整数表示 value) */
static int v_mutex = 1, v_empty = 3, v_full = 0;
static int step_no = 0;
static void P(const char *who, int *v, const char *name) {
(*v)--; /* 先记账 */
step_no++;
printf("%2d. mutex=%2d empty=%2d full=%2d | %s P(%-6s)%s\n",
step_no, v_mutex, v_empty, v_full, who, name, (*v < 0) ? " -> 阻塞" : "");
}
static void V(const char *who, int *v, const char *name) {
(*v)++; /* 先记账 */
step_no++;
printf("%2d. mutex=%2d empty=%2d full=%2d | %s V(%-6s)%s\n",
step_no, v_mutex, v_empty, v_full, who, name, (*v <= 0) ? " -> 唤醒一个等待者" : "");
}
static void ACT(const char *who, const char *what) {
step_no++;
printf("%2d. mutex=%2d empty=%2d full=%2d | %s %s\n",
step_no, v_mutex, v_empty, v_full, who, what);
}
int main(void) {
printf("生产者-消费者: 缓冲区 3, 初值 mutex=1 empty=3 full=0\n");
P("生产者", &v_empty, "empty"); /* 1 生产者占一个空位 */
P("消费者", &v_full, "full"); /* 2 消费者无产品可拿 -> 阻塞 */
P("生产者", &v_mutex, "mutex"); /* 3 生产者拿缓冲区锁 */
ACT("生产者", "放入产品"); /* 4 */
V("生产者", &v_mutex, "mutex"); /* 5 放锁 */
V("生产者", &v_full, "full"); /* 6 唤醒消费者 */
P("消费者", &v_mutex, "mutex"); /* 7 消费者拿锁 */
P("生产者", &v_empty, "empty"); /* 8 生产者再占一个空位 */
ACT("消费者", "取走产品"); /* 9 */
P("生产者", &v_mutex, "mutex"); /* 10 锁在消费者手里 -> 阻塞 */
V("消费者", &v_mutex, "mutex"); /* 11 唤醒生产者 */
ACT("生产者", "放入产品"); /* 12 生产者恢复后放入 */
printf("自检: empty + max(full,0) + 临界区中的产品 = 3\n");
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
生产者-消费者: 缓冲区 3, 初值 mutex=1 empty=3 full=0
1. mutex= 1 empty= 2 full= 0 | 生产者 P(empty )
2. mutex= 1 empty= 2 full=-1 | 消费者 P(full ) -> 阻塞
3. mutex= 0 empty= 2 full=-1 | 生产者 P(mutex )
4. mutex= 0 empty= 2 full=-1 | 生产者 放入产品
5. mutex= 1 empty= 2 full=-1 | 生产者 V(mutex )
6. mutex= 1 empty= 2 full= 0 | 生产者 V(full ) -> 唤醒一个等待者
7. mutex= 0 empty= 2 full= 0 | 消费者 P(mutex )
8. mutex= 0 empty= 1 full= 0 | 生产者 P(empty )
9. mutex= 0 empty= 1 full= 0 | 消费者 取走产品
10. mutex=-1 empty= 1 full= 0 | 生产者 P(mutex ) -> 阻塞
11. mutex= 0 empty= 1 full= 0 | 消费者 V(mutex ) -> 唤醒一个等待者
12. mutex= 0 empty= 1 full= 0 | 生产者 放入产品
自检: empty + max(full,0) + 临界区中的产品 = 3三点说明:
- 中文一律放在格式串最后(
%s在尾部)——printf对中文按"字节"补齐,中文放前面必然错位(os/10-process.md例 3 已踩过)。 - 第 2 步
full = −1是"1 个消费者在等",不是"负一个产品"——这就是"负值绝对值 = 等待进程数"。 - 本程序把"一次合法交错"按顺序演一遍;真实系统里这是由调度器决定的,但"任何合法交错的 PV 轨迹都必须满足
P/V的记账规则"——这才是考点。
例 6:Python——三大问题的模拟与死锁检测
def pv_sim(progs, init, max_steps=200):
"""离散事件模拟: 每轮按进程顺序各走一步; 阻塞者跳过; 全部阻塞即死锁"""
val, waitq = dict(init), {k: [] for k in init}
pc, blocked, log = [0] * len(progs), [None] * len(progs), []
puts = gets = 0
for _ in range(max_steps):
moved = False
for i, (name, ops) in enumerate(progs):
if blocked[i] is not None:
continue
op = ops[pc[i] % len(ops)]
if op[0] == 'P':
s = op[1]; val[s] -= 1
if val[s] < 0:
waitq[s].append(i); blocked[i] = s
log.append((name, 'P(%s)' % s, '阻塞'))
else:
log.append((name, 'P(%s)' % s, '通过'))
elif op[0] == 'V':
s = op[1]; val[s] += 1
if val[s] <= 0 and waitq[s]:
blocked[waitq[s].pop(0)] = None
log.append((name, 'V(%s)' % s, '唤醒一个'))
else:
log.append((name, 'V(%s)' % s, '通过'))
else:
log.append((name, op[1], '执行'))
if op[1] == '放入产品': puts += 1
if op[1] == '取走产品': gets += 1
pc[i] += 1; moved = True
if not moved:
log.append(('--', '全部阻塞=死锁', ''))
break
return val, log, puts, gets
PROD = [('P', 'empty'), ('P', 'mutex'), ('ACT', '放入产品'), ('V', 'mutex'), ('V', 'full')]
CONS = [('P', 'full'), ('P', 'mutex'), ('ACT', '取走产品'), ('V', 'mutex'), ('V', 'empty')]
PROD_BAD = [('P', 'mutex'), ('P', 'empty'), ('ACT', '放入产品'), ('V', 'mutex'), ('V', 'full')]
print('=== 生产者-消费者: 缓冲区 3 ===')
val, log, puts, gets = pv_sim([('生产者', PROD), ('消费者', CONS)], {'mutex': 1, 'empty': 3, 'full': 0}, 60)
print(' 跑 60 轮: 放入 %d 个, 取走 %d 个, 库存 %d (<=3 -> %s)'
% (puts, gets, puts - gets, puts - gets <= 3))
print('=== P 顺序颠倒 + 缓冲区已满(empty=0, full=3) ===')
val, log, _, _ = pv_sim([('生产者', PROD_BAD), ('消费者', CONS)], {'mutex': 1, 'empty': 0, 'full': 3}, 60)
for r in log[:5]:
print(' %-8s %-10s %s' % r)
print(' 结局: %s' % log[-1][1])
print('=== P 顺序正确, 同一初始状态 ===')
val, log, puts, gets = pv_sim([('生产者', PROD), ('消费者', CONS)], {'mutex': 1, 'empty': 0, 'full': 3}, 60)
for r in log[:6]:
print(' %-8s %-10s %s' % r)
print(' 结局: %s (放入 %d 取走 %d)'
% ('死锁' if log[-1][1].endswith('死锁') else '未死锁', puts, gets))
print('=== 哲学家: 5 人先左后右 ===')
PH = [('P%d' % i, [('P', 'c%d' % i), ('P', 'c%d' % ((i + 1) % 5)), ('ACT', '进餐'),
('V', 'c%d' % i), ('V', 'c%d' % ((i + 1) % 5))]) for i in range(5)]
val, log, _, _ = pv_sim(PH, {'c0': 1, 'c1': 1, 'c2': 1, 'c3': 1, 'c4': 1}, 60)
print(' 前 5 步: %s' % ' | '.join('%s %s %s' % r for r in log[:5]))
print(' 结局: %s, 五根筷子 value = %s' % (log[-1][1], val))
print('=== 哲学家: 偶数先左、奇数先右 ===')
PH2 = [('P%d' % i, [('P', 'c%d' % (i if i % 2 == 0 else (i + 1) % 5)),
('P', 'c%d' % (((i + 1) % 5) if i % 2 == 0 else i)), ('ACT', '进餐'),
('V', 'c%d' % i), ('V', 'c%d' % ((i + 1) % 5))]) for i in range(5)]
val, log, _, _ = pv_sim(PH2, {'c0': 1, 'c1': 1, 'c2': 1, 'c3': 1, 'c4': 1}, 120)
print(' 跑 120 轮: %s, 进餐次数 = %d'
% ('无死锁' if not log[-1][1].endswith('死锁') else '死锁', sum(1 for r in log if r[1] == '进餐')))
print('=== 缓冲区为 1 时 mutex 可省: 自检 ===')
for empty, full in [(1, 0), (0, 1)]:
print(' empty=%d full=%d -> 生产者可进吗? %s; 消费者可进吗? %s'
% (empty, full, empty > 0, full > 0))
print(' -> 两者永远不可能同时进 -> 互斥自然成立 (n=1 时 mutex 可省)')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照:正确顺序跑 60 轮得"放入 12 个、取走 11 个、库存 1(≤3 成立)";P 顺序颠倒 + 缓冲区已满 → 结局"全部阻塞=死锁";P 顺序正确 + 同一初始状态 → "未死锁(放入 11 取走 12)";哲学家朴素版 → 死锁,五根筷子 value 全为 −1;奇偶顺序版跑 120 轮 → 无死锁、进餐 85 次;缓冲区为 1 时自检得"两者永远不可能同时进"。全部与手算一致。
⚠️ 计数口径提醒:本模拟的
max_steps是**"轮次"**(每轮让每个未阻塞的进程各走一步),不是"总指令数"。所以"跑 60 轮"并不等于"执行 60 条 PV"——要报"执行了多少条 PV",应该数日志长度而不是数轮次。
考点
考点
1. 必背结论
- 生产者-消费者三信号量:
mutex = 1(互斥)、empty = n(空位数)、full = 0(产品数)。 - P 顺序铁律:先
P(empty/full)(同步),后P(mutex)(互斥);两个V的顺序无所谓。 - 缓冲区
时可以省掉mutex(因为empty与full互补,两人不可能同时进); 时不能省。 - 读者-写者(读者优先)三要素:
rw = 1、mutex = 1、count = 0;"第一个读者P(rw)、最后一个读者V(rw)";count必须用mutex保护。 - 读者优先 → 写者饥饿;写者优先要加挡板信号量
w,代价是读者可能饥饿。 - 哲学家朴素解的后果:5 人各持一根筷子 → 循环等待 → 死锁。
- 四种解法:限制人数(初值 4 的
num)、奇偶顺序(先拿方向相反)、一次拿两只(AND 型信号量Swait)、拿筷子时加全局互斥。 - AND 型信号量的语义:"要么全拿到、要么一个都不占"——直接消灭"部分占有"这个死锁条件。
value < 0时 = 等待进程数;empty + full只有在"无人阻塞"时才等于 。
2. 高频陷阱
- 把
P(mutex)写在P(empty)前面:错。会"持锁等资源"→ 死锁(例 2 有完整轨迹)。 - 以为"两个
V的顺序也不能反":错。V只是加法与唤醒,顺序随意。(这是"P 序严格、V 序自由"的不对称性的来源。) - 说"缓冲区为 1 时
mutex也不能省":错,能省——但要能说清理由(empty/full互补,不可能同时进)。 - 把
full = -1当成"产品数为负数":错。它是"1 个消费者在等"。 - 读者-写者里忘记用
mutex保护count:错。count++/count--不是原子的,两个读者可能都读到 0 而同时P(rw)。 - 把"读者优先"的
P(rw)写在每个读者里:错。只有count == 1的第一个读者才P(rw);否则读者之间就变成互斥了,违反"读读可共享"。 - 说"哲学家先左后右一定死锁":严格说不是"一定",而是"存在死锁的交错"(恰好 5 人同时拿起同一侧时就死)。答题写"可能死锁 / 存在死锁交错"更准确。
- 用"限制人数 4"时说"因为筷子只有 5 根":错。理由是"最多 4 人争 5 根 → 必有一人能凑齐两根 → 环不可能闭合"。
- PV 数量不等却以为写对了:错。"每个信号量的 P 与 V 出现次数必须相等"是自检的第一条(总量守恒)。
- 忘记写"信号量初值":408 的 PV 大题按初值、配对、顺序三处给分,漏一个就丢分。
3. 解题模板("PV 设计大题",四步走)
① 找"独占资源" -> 每类一个 mutex, 初值 1
(缓冲区本身、count 变量、筷子、rw 锁…)
② 找"先后次序" -> 每条约束一个同步信号量, 初值 0 或资源数
(empty=n, full=0 这类"计数"信号量属于同步)
③ 写代码骨架, P 序"先同步后互斥"; 每个信号量 P/V 数量相等
④ 三条自检:
- empty + full 是否恒等于 n (无人阻塞时)?
- 有没有"持锁等待"的 P 顺序?
- 第一个/最后一个的"边界判断"是否写对 (count==1 / count==0)?4. 与相邻章节的接口
os/13-semaphore.md(信号量与 PV):本篇全部是那篇规则的用例;empty/full是"资源计数信号量",mutex是"互斥信号量"。os/12-sync.md(同步与互斥):互斥是"抢资源"、同步是"讲次序"——生产者-消费者是两者混合的典型。os/15-monitor.md(管程):管程把本节的mutex隐掉了(编译器自动加锁),只留下条件变量notFull/notEmpty——代码量骤减,考点也变。os/16-deadlock.md(死锁):例 2 与例 4 都是死锁现场;四个必要条件(互斥、请求并保持、不可剥夺、循环等待)在这里全部能找到活标本。os/11-scheduling.md(调度):P阻塞触发调度、V唤醒只是入就绪队列——PV 与调度的接力关系贯穿全篇。
小结
- 生产者-消费者三信号量:
mutex = 1、empty = n、full = 0;P 序"先empty/full后mutex",V 序随意; 时mutex可省。 - 例 1 的 12 步轨迹演示了**"合法交错下 PV 的记账";例 2 的两张对照表演示了"P 顺序颠倒 → 双方各握一半、环闭合 → 死锁"**。
- 读者-写者的全部技巧就两句:
count == 1时第一个读者上rw锁、count == 0时最后一个读者放锁;count必须用mutex保护。 - 读者优先 → 写者饥饿;写者优先靠"挡板
w",代价是读者饥饿——"优先"从来都是把饥饿转嫁给另一方。 - 哲学家朴素解"5 人各持一根 → 死锁";四种解法里,"一次拿两只(AND 型)"最优雅(消灭部分占有),"限制人数 4"最好写。
- 自检三句:PV 数量相等吗?有持锁等待吗?
empty + full对得上吗?
下一篇:管程
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。