补充:用信号量实现前驱关系
一:实现进程的前驱关系(多级同步问题)
进程的前驱关系,本质是 “多个进程间存在先后依赖”—— 比如进程 A 执行完才能执行进程 B,进程 B 执行完才能执行进程 C,就像 “流水线作业”,必须按顺序推进。而每一对 “前 - 后” 依赖,都是一个独立的同步问题,因此信号量的核心作用就是为每一对前驱关系 “保驾护航”。
1. 核心原理
实现前驱关系需遵循三步:
- 画前驱图:先梳理进程间的依赖关系,用箭头表示 “前操作→后操作”(箭头起点是 “前进程”,终点是 “后进程”);
- 设信号量:为每一对前驱关系设置一个同步信号量,初值均为 0(理由同同步问题:初始时 “后进程” 需等待 “前进程” 的触发信号);
- 加 P/V 操作:在 “前操作” 结束后执行 V 操作(释放信号,通知 “后进程” 可执行),在 “后操作” 开始前执行 P 操作(等待 “前进程” 的信号)。
2. 案例实操:三进程前驱关系
假设存在三个进程 P1、P2、P3,依赖关系为:P1 执行完→P2 和 P3 才能执行(前驱图如下),用信号量实现该逻辑。
(1)第一步:画前驱图
P1 → S1 → P2↓S2 → P3
- 箭头 1:P1→P2,对应同步信号量 S1;
- 箭头 2:P1→P3,对应同步信号量 S2。
(2)第二步:初始化信号量
根据规则,每对前驱关系的信号量初值为 0:
semaphore S1 = {0, NULL}; // 控制P1→P2的前驱关系
semaphore S2 = {0, NULL}; // 控制P1→P3的前驱关系
(3)第三步:添加 P/V 操作
- P1(前进程):执行完自身任务后,需触发 P2 和 P3,因此在任务结束后执行 V (S1) 和 V (S2);
- P2(后进程):需等待 P1 完成,因此在任务开始前执行 P (S1);
- P3(后进程):需等待 P1 完成,因此在任务开始前执行 P (S2)。
代码逻辑如下:
// 进程P1(前进程,触发P2和P3)
void P1() {执行P1的任务... // 如“读取数据到内存”V(S1); // 释放S1,通知P2“P1已完成”V(S2); // 释放S2,通知P3“P1已完成”
}// 进程P2(后进程,等待P1)
void P2() {P(S1); // 等待S1信号(P1未执行完则阻塞)执行P2的任务... // 如“处理P1读取的数据”
}// 进程P3(后进程,等待P1)
void P3() {P(S2); // 等待S2信号(P1未执行完则阻塞)执行P3的任务... // 如“备份P1读取的数据”
}
(4)逻辑验证
- 初始时 S1=0、S2=0,P2 执行 P (S1) 后阻塞,P3 执行 P (S2) 后阻塞;
- P1 执行完任务后,V (S1) 使 S1=1(P2 被唤醒,开始执行),V (S2) 使 S2=1(P3 被唤醒,开始执行);
- 最终实现 “P1 完→P2、P3 同时执行” 的前驱关系,符合预期。
3. 进阶案例:四进程多级前驱关系
若进程依赖为:P1→P2→P3,P1→P4(前驱图如下),实现逻辑如下:
(1)前驱图与信号量设置
P1 → S1 → P2 → S3 → P3↓S2 → P4
- 前驱对 1:P1→P2 → 信号量 S1(初值 0);
- 前驱对 2:P1→P4 → 信号量 S2(初值 0);
- 前驱对 3:P2→P3 → 信号量 S3(初值 0)。
(2)代码逻辑
semaphore S1=0, S2=0, S3=0;void P1() {执行P1任务...V(S1); // 触发P2V(S2); // 触发P4
}void P2() {P(S1); // 等P1执行P2任务...V(S3); // 触发P3
}void P3() {P(S3); // 等P2执行P3任务...
}void P4() {P(S2); // 等P1执行P4任务...
}
结论:无论多少级前驱关系,核心都是 “一对前驱对应一个信号量,前 V 后 P”,逻辑可无限复用。
八、多资源场景:信号量初值的灵活设置
之前的案例中,信号量初值非 0 即 1,但实际系统中常存在 “多份同类资源”(如 3 台打印机、5 个缓冲区),此时信号量的初值需等于资源总数,而非固定 0 或 1。
1. 核心
- 若信号量表示 “资源数量”:初值 = 系统中该资源的总数量;
- P 操作:申请 1 份资源(S--,S<0 则阻塞,等待其他进程释放);
- V 操作:释放 1 份资源(S++,S<=0 则唤醒 1 个等待进程)。
2. 案例:3 台打印机的资源分配
系统有 3 台打印机(资源总数 = 3),用信号量实现多进程的资源申请与释放:
// 初始化信号量:初值=3(3台打印机)
semaphore printer = {3, NULL};// 进程A申请打印机
void ProcessA() {P(printer); // 申请1台,printer=3-1=2(足够,直接使用)使用打印机...V(printer); // 释放1台,printer=2+1=3
}// 进程B申请打印机
void ProcessB() {P(printer); // 申请1台,printer=3-1=2(足够)使用打印机...V(printer);
}// 进程C申请打印机
void ProcessC() {P(printer); // 申请1台,printer=3-1=2(足够)使用打印机...V(printer);
}// 进程D申请打印机(前3台被占用)
void ProcessD() {P(printer); // 申请1台,printer=3-4=-1(不足,阻塞)使用打印机... // 需等待A/B/C中任意一个释放V(printer);
}
注意:多资源场景下,信号量初值≠0/1,需根据资源总数设置,这是考试中区分 “单资源” 与 “多资源” 的关键标志。
九、信号量应用的易错点
1. 错误 1:P/V 操作不成对
- 缺 P 操作:多个进程可同时进入临界区,互斥失效(如打印机被多个进程同时使用);
- 缺 V 操作:信号量永远无法恢复初值,等待进程永久阻塞(如 mutex=0 后无 V 操作,所有进程卡在 P (mutex));
- 避坑:写代码时先标注 “临界区 / 前操作 / 后操作”,再强制在对应位置添加 P/V,写完后检查成对性。
2. 错误 2:信号量初值设置错误
- 互斥信号量设为 0:初始时无进程能进入临界区(P (mutex) 直接阻塞);
- 同步信号量设为 1:“后进程” 无需等待,直接执行,同步关系失效;
- 多资源信号量设为 1:仅能分配 1 份资源,浪费其他资源;
- 避坑:牢记 “三对应”—— 互斥→1,同步 / 前驱→0,多资源→资源总数。
3. 错误 3:P/V 操作顺序颠倒
- 互斥场景:先 V 后 P(如先 V (mutex) 再 P (mutex)),会导致多个进程同时进入临界区;
- 同步场景:先 P 后 V(如 “后进程” 先 P,“前进程” 后 V 是正确的,但 “前进程” 先 P 则会阻塞);
- 避坑:按 “申请→使用→释放” 逻辑:互斥(P→临界区→V),同步(前 V→后 P)。