

好的,我们来详细解析这道2015年的考研真题。这道题是一个非常巧妙的“生产者-消费者”模型的变体,它将两个进程设置为互为生产者和消费者的角色,深刻考察了对模型本质的理解。
第一步:重述题目原文
【2015 统考真题】有 A, B 两人通过信箱进行辩论,每个人都从自己的信箱中取得对方的问题。将答案和向对方提出的新问题组成一个邮件放入对方的信箱中。假设 A 的信箱最多放 M 个邮件,B 的信箱最多放 N 个邮件。初始时 A 的信箱中有 x 个邮件 (0 < x < M),B 的信箱中有 y 个邮件 (0 < y < N)。辩论者每取出一个邮件,邮件数减 1。
当信箱不为空时,辩论者才能从信箱中取邮件,否则等待。当信箱不满时,辩论者才能将新邮件放入信箱,否则等待。请添加必要的信号量和 P, V 操作,以实现上述过程的同步。要求写出完整的过程,并说明信号量的含义和赋初值。
第二步:知识体系是什么?
这道题的本质是 两个耦合的、独立的生产者-消费者系统。
识别双重角色:
进程 A 的行为是:从自己的信箱 A 中 消费 邮件,然后向对方的信箱 B 中 生产 邮件。进程 B 的行为是:从自己的信箱 B 中 消费 邮件,然后向对方的信箱 A 中 生产 邮件。所以,A 和 B 互为生产者和消费者。A 是信箱 B 的生产者和信箱 A 的消费者;B 是信箱 A 的生产者和信箱 B 的消费者。
识别两个独立的缓冲区:
信箱 A 是一个独立的共享缓冲区,容量为 M。信箱 B 是另一个独立的共享缓冲区,容量为 N。这两个缓冲区是完全独立的资源,对其中一个的操作不应该影响另一个。
核心模型:
既然有两个独立的缓冲区,我们就需要为每一个缓冲区都配置一套完整的、标准的生产者-消费者解决方案。一套标准的解决方案包括:一个互斥锁 (),一个计数空位的信号量 (
mutex),一个计数产品的信号量 (
empty)。
full
第三步:解题套路是什么?
套路一:分解系统,独立分析
不要把 A 和 B 的交互看成一个复杂的整体。而是分解成两个独立的子问题:
子问题1:如何管理信箱 A?子问题2:如何管理信箱 B?
套路二:为每个子问题配置“三件套”信号量
分析信箱 A (容量M, 初始x封):
互斥:任何时候只能有一个进程(A在取,或B在放)操作信箱A。 -> 定义 同步 (消费者A):A 要取信,必须等信箱A不为空。-> 定义
semaphore mutex_A = 1; (记录信箱A中的邮件数)同步 (生产者B):B 要放信,必须等信箱A不满。-> 定义
semaphore full_A = x; (记录信箱A中的空位数)
semaphore empty_A = M - x;
分析信箱 B (容量N, 初始y封):
互斥:任何时候只能有一个进程(B在取,或A在放)操作信箱B。 -> 定义 同步 (消费者B):B 要取信,必须等信箱B不为空。-> 定义
semaphore mutex_B = 1; (记录信箱B中的邮件数)同步 (生产者A):A 要放信,必须等信箱B不满。-> 定义
semaphore full_B = y; (记录信箱B中的空位数)
semaphore empty_B = N - y;
套路三:组合流程,安排P/V操作
进程 A 的完整流程:
从自己信箱(A)取信 (作为消费者):
// 等待信箱A有信
P(full_A) // 锁定信箱A从信箱A取出一个邮件
P(mutex_A) // 解锁信箱A
V(mutex_A) // 通知信箱A空位数+1
V(empty_A)
回答问题并提出新问题。向对方信箱(B)放信 (作为生产者):
// 等待信箱B有空位
P(empty_B) // 锁定信箱B将新邮件放入信箱B
P(mutex_B) // 解锁信箱B
V(mutex_B) // 通知信箱B邮件数+1
V(full_B)
进程 B 的完整流程:与进程 A 完全对称,只需交换 A 和 B 即可。
第四步:为什么是这样?(深度剖析)
为什么需要两个互斥锁 (,
mutex_A)?
mutex_B
因为信箱 A 和信箱 B 是两个物理上和逻辑上都独立的资源。进程 A 正在操作信箱 B 时,不应该阻止进程 B 操作信箱 A。如果只用一个全局 ,当 A 锁住它去访问 B 的信箱时,B 就无法访问自己的信箱 A,大大降低了并发性,甚至可能导致死锁。
mutex
为什么初始值不是0或M/N?
这是本题的一个关键细节。题目明确给出了“初始时A的信箱中有x个邮件,B有y个邮件”。信号量必须精确地反映资源的初始状态。所以 信号量直接对应初始的产品数
full 和
x,而
y 信号量则对应初始的空位数
empty 和
M-x。
N-y
P/V操作的配对逻辑
对于信箱A: (由A执行) 和
P(full_A) (由B执行) 是一对,它们共同管理着
V(full_A) 这个计数器。同样,
full_A (由B执行) 和
P(empty_A) (由A执行) 是一对。这个“交叉”关系正是“互为生产者-消费者”的体现。
V(empty_A)
死锁检查
再次应用黄金法则:“同步P在互斥P之前”。在进程A的代码中, 在
P(full_A) 之前,
P(mutex_A) 在
P(empty_B) 之前。这确保了进程不会在持有锁(一种资源)的同时,去等待另一种资源(产品或空位),从而避免了死锁。
P(mutex_B)
第五步:考试怎么解决?(实战指南)
识别模型:看到“两个人/进程”、“两个信箱/缓冲区”,立刻意识到这是“双缓冲区、互为生产者-消费者”模型。分解资源:在草稿纸上画两个圈,分别标注“信箱A”和“信箱B”。独立建模:
在“信箱A”旁边写下它的三件套:,
mutex_A,
full_A。并根据题目的
empty_A 和
M 标出初值:
x,
1,
x。在“信箱B”旁边做同样的事:
M-x,
mutex_B,
full_B。标出初值:
empty_B,
1,
y。
N-y
构建一个进程的逻辑:只看进程A。
第一步:消费。从哪消费?信箱A。写下消费四步曲:,
P(full_A),
P(mutex_A),
...,
V(mutex_A)。第二步:生产。向哪生产?信箱B。写下生产四步曲:
V(empty_A),
P(empty_B),
P(mutex_B),
...,
V(mutex_B)。
V(full_B)
对称构建另一个进程:将进程A的代码复制一份,把所有的A换成B,B换成A,就得到了进程B的代码。最后整合:把信号量定义和 框架写好,填入代码,完成。
cobegin
【完整标准答案】
1. 信号量定义及含义
互斥信号量 :
mutex_A
含义: 用于保证进程 A 和 B 对信箱 A 的互斥访问。初值: 1。
互斥信号量 :
mutex_B
含义: 用于保证进程 A 和 B 对信箱 B 的互斥访问。初值: 1。
资源信号量 :
full_A
含义: 记录信箱 A 中当前邮件的数量。初值: x。
资源信号量 :
empty_A
含义: 记录信箱 A 中当前空闲位置的数量。初值: M – x。
资源信号量 :
full_B
含义: 记录信箱 B 中当前邮件的数量。初值: y。
资源信号量 :
empty_B
含义: 记录信箱 B 中当前空闲位置的数量。初值: N – y。
2. 进程同步与互斥的伪代码实现
// 信号量定义及初始化
semaphore mutex_A = 1, mutex_B = 1;
semaphore full_A = x, empty_A = M - x;
semaphore full_B = y, empty_B = N - y;
cobegin {
// 辩论者 A 的进程
process A {
while(TRUE) {
// 从自己的信箱A中取邮件
P(full_A); // 等待信箱A中有邮件
P(mutex_A); // 锁定信箱A
// 从A的信箱中取出一个邮件;
V(mutex_A); // 解锁信箱A
V(empty_A); // 通知信箱A的空位数加一
// 回答问题并提出一个新问题;
// 将新邮件放入B的信箱
P(empty_B); // 等待信箱B中有空位
P(mutex_B); // 锁定信箱B
// 将新邮件放入B的信箱;
V(mutex_B); // 解锁信箱B
V(full_B); // 通知信箱B的邮件数加一
}
}
// 辩论者 B 的进程
process B {
while(TRUE) {
// 从自己的信箱B中取邮件
P(full_B); // 等待信箱B中有邮件
P(mutex_B); // 锁定信箱B
// 从B的信箱中取出一个邮件;
V(mutex_B); // 解锁信箱B
V(empty_B); // 通知信箱B的空位数加一
// 回答问题并提出一个新问题;
// 将新邮件放入A的信箱
P(empty_A); // 等待信箱A中有空位
P(mutex_A); // 锁定信箱A
// 将新邮件放入A的信箱;
V(mutex_A); // 解锁信箱A
V(full_A); // 通知信箱A的邮件数加一
}
}
} coend


