2.3进程同步与互斥
一、同步与互斥的基本概念
1.1 进程同步
1.1.1 异步性问题
异步性定义:各并发执行的进程以各自独立的、不可预知的速度向前推进
同步机制作用:操作系统提供"进程同步机制"解决异步问题
1.1.2 同步需求场景
进程A:读数据 → 数据预处理 → 写回缓冲区 → 其他任务 进程B:准备工作 → 读数据 → 数据后续处理
需要保证进程B在进程A完成数据预处理后才能读取数据
1.2 进程互斥
1.2.1 资源共享方式
互斥共享方式:一个时间段内只允许一个进程访问该资源
同时共享方式:允许一个时间段内由多个进程"同时"访问
1.2.2 临界资源与临界区
临界资源:一个时间段内只允许一个进程使用的资源
物理设备:打印机、摄像头等
软件资源:变量、数据、内存缓冲区等
临界区:访问临界资源的那段代码
1.2.3 进程互斥的四个部分
do {entry section; // 进入区:检查是否可进入临界区,设置访问标志critical section; // 临界区:访问临界资源的代码段exit section; // 退出区:解除访问标志remainder section; // 剩余区:其他处理
} while(true);
1.2.4 进程互斥原则
空闲让进:临界区空闲时,允许请求进入的进程立即进入
忙则等待:当已有进程进入临界区时,其他试图进入的进程必须等待
有限等待:对请求访问的进程,应保证在有限时间内进入临界区
让权等待:当进程不能进入临界区时,应立即释放处理机,防止忙等待
二、进程互斥的软件实现方法
2.1 单标志法
int turn = 0; // 表示当前允许进入临界区的进程号// P0进程
while(turn != 0); // 进入区
critical_section; // 临界区
turn = 1; // 退出区// P1进程
while(turn != 1); // 进入区
critical_section; // 临界区
turn = 0; // 退出区
优点:实现简单
缺点:违背"空闲让进"原则,必须轮流访问
2.2 双标志先检查法
bool flag[2] = {false, false};// P0进程
while(flag[1]); // 检查
flag[0] = true; // 上锁
critical_section;
flag[0] = false; // 解锁// P1进程
while(flag[0]);
flag[1] = true;
critical_section;
flag[1] = false;
问题:违反"忙则等待",检查与上锁非原子操作
2.3 双标志后检查法
bool flag[2] = {false, false};// P0进程
flag[0] = true; // 先上锁
while(flag[1]); // 后检查
critical_section;
flag[0] = false;// P1进程
flag[1] = true;
while(flag[0]);
critical_section;
flag[1] = false;
问题:违反"空闲让进"和"有限等待",可能导致饥饿
2.4 Peterson算法
bool flag[2] = {false, false};
int turn = 0;// P0进程
flag[0] = true; // 表达意愿
turn = 1; // 主动谦让
while(flag[1] && turn == 1); // 检查
critical_section;
flag[0] = false;// P1进程
flag[1] = true;
turn = 0;
while(flag[0] && turn == 0);
critical_section;
flag[1] = false;
优点:满足空闲让进、忙则等待、有限等待
缺点:不满足让权等待,会发生忙等
三、进程互斥的硬件实现方法
3.1 中断屏蔽方法
关中断();
临界区代码;
开中断();
优点:简单、高效
缺点:只适用于单处理机;只适用于操作系统内核进程
3.2 TestAndSet指令(TSL指令)
bool TestAndSet(bool *lock) {bool old = *lock;*lock = true;return old;
}// 使用
while(TestAndSet(&lock));
临界区代码;
lock = false;
优点:实现简单;适用于多处理机环境
缺点:不满足"让权等待"
3.3 Swap指令(XCHG指令)
void Swap(bool *a, bool *b) {bool temp = *a;*a = *b;*b = temp;
}// 使用
bool old = true;
while(old == true)Swap(&lock, &old);
临界区代码;
lock = false;
特性:逻辑上与TSL指令类似
优缺点:同TSL指令
四、互斥锁
4.1 互斥锁基本概念
acquire() {while(!available); // 忙等待available = false; // 获得锁
}release() {available = true; // 释放锁
}
4.2 自旋锁特性
定义:需要连续循环忙等的互斥锁
包含:TSL指令、Swap指令、单标志法
优点:等待期间不用切换进程上下文
缺点:违反"让权等待"
适用:多处理器系统,上锁时间短的场景
五、信号量机制
5.1 信号量基本概念
提出者:Dijkstra(1965年)
本质:变量,表示系统中某种资源的数量
操作:wait(S)和signal(S)原语,简称P、V操作
5.2 整型信号量
int S = 1; // 初始化信号量void wait(int S) {while(S <= 0); // 忙等待S = S - 1;
}void signal(int S) {S = S + 1;
}
问题:不满足"让权等待",会发生忙等
5.3 记录型信号量
typedef struct {int value; // 剩余资源数struct process *L; // 等待队列
} semaphore;void wait(semaphore S) {S.value--;if(S.value < 0) {block(S.L); // 自我阻塞}
}void signal(semaphore S) {S.value++;if(S.value <= 0) {wakeup(S.L); // 唤醒等待进程}
}
优点:满足"让权等待"
注意:考试中默认信号量为记录型信号量
六、用信号量实现进程互斥、同步、前驱关系
6.1 实现进程互斥
semaphore mutex = 1; // 互斥信号量P1() {P(mutex); // 加锁临界区代码;V(mutex); // 解锁
}P2() {P(mutex);临界区代码;V(mutex);
}
要点:
对不同的临界资源设置不同的互斥信号量
P、V操作必须成对出现
6.2 实现进程同步
semaphore S = 0; // 同步信号量// 前操作进程
P1() {代码1;代码2;V(S); // 前V代码3;
}// 后操作进程
P2() {P(S); // 后P代码4;代码5;代码6;
}
口诀:前V后P
原理:信号量S代表"某种资源",由前操作产生,后操作消耗
6.3 实现前驱关系
S1/ \S2 S3| |S4 S5\ /S6
semaphore a, b, c, d, e, f, g = 0;// 分别为每一对前驱关系设置同步信号量
S1() { ... V(a); V(b); }
S2() { P(a); ... V(c); }
S3() { P(b); ... V(d); }
S4() { P(c); ... V(e); }
S5() { P(d); ... V(f); }
S6() { P(e); P(f); ... }
七、生产者-消费者问题
7.1 问题描述
生产者:生产产品放入缓冲区
消费者:从缓冲区取出产品使用
缓冲区:初始为空,大小为n的临界资源
约束:
缓冲区未满时,生产者才能放入产品
缓冲区未空时,消费者才能取出产品
缓冲区必须互斥访问
7.2 信号量设置
semaphore mutex = 1; // 互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; // 同步信号量,表示空闲缓冲区数量
semaphore full = 0; // 同步信号量,表示产品数量
7.3 实现代码
producer() {while(1) {生产一个产品;P(empty); // 消耗一个空闲缓冲区P(mutex);把产品放入缓冲区;V(mutex);V(full); // 增加一个产品}
}consumer() {while(1) {P(full); // 消耗一个产品P(mutex);从缓冲区取出一个产品;V(mutex);V(empty); // 增加一个空闲缓冲区使用产品;}
}
7.4 注意事项
P操作顺序:实现同步的P操作要在实现互斥的P操作之前,否则可能死锁
V操作顺序:可以交换,因为V操作不会导致进程阻塞
八、多生产者-多消费者问题
8.1 问题描述(水果盘子问题)
生产者:爸爸(放苹果)、妈妈(放橘子)
消费者:儿子(吃橘子)、女儿(吃苹果)
缓冲区:盘子,容量为1
约束:
盘子空时,爸爸或妈妈才能放水果
盘中有对应水果时,儿子或女儿才能取水果
8.2 信号量设置
semaphore mutex = 1; // 互斥访问盘子
semaphore apple = 0; // 盘子中苹果数量
semaphore orange = 0; // 盘子中橘子数量
semaphore plate = 1; // 盘子中可放水果数量
8.3 实现代码
dad() {while(1) {准备一个苹果;P(plate);P(mutex);把苹果放入盘子;V(mutex);V(apple);}
}mom() {while(1) {准备一个橘子;P(plate);P(mutex);把橘子放入盘子;V(mutex);V(orange);}
}daughter() {while(1) {P(apple);P(mutex);从盘中取出苹果;V(mutex);V(plate);吃掉苹果;}
}son() {while(1) {P(orange);P(mutex);从盘中取出橘子;V(mutex);V(plate);吃掉橘子;}
}
8.4 特殊情况分析
缓冲区大小为1:可能不需要互斥信号量(但考试建议加上)
缓冲区大小>1:必须设置互斥信号量
分析方法:从"事件"角度分析,而非单个进程行为
九、读者-写者问题
9.1 问题描述
读者:读文件,允许多个读者同时读
写者:写文件,只允许一个写者写
约束:
允许多个读者同时对文件执行读操作
只允许一个写者往文件中写信息
任一写者在完成写操作前不允许其他读者或写者工作
写者执行写操作前,应让已有的读者和写者全部退出
9.2 基本实现(读者优先)
semaphore rw = 1; // 实现对文件的互斥访问
int count = 0; // 当前读进程数
semaphore mutex = 1; // 保证对count的互斥访问writer() {while(1) {P(rw); // 写前加锁写文件;V(rw); // 写后解锁}
}reader() {while(1) {P(mutex);if(count == 0) // 第一个读进程负责加锁P(rw);count++;V(mutex);读文件;P(mutex);count--;if(count == 0) // 最后一个读进程负责解锁V(rw);V(mutex);}
}
9.3 写者优先实现
semaphore rw = 1;
int count = 0;
semaphore mutex = 1;
semaphore w = 1; // 实现写优先writer() {while(1) {P(w);P(rw);写文件;V(rw);V(w);}
}reader() {while(1) {P(w);P(mutex);if(count == 0)P(rw);count++;V(mutex);V(w);读文件;P(mutex);count--;if(count == 0)V(rw);V(mutex);}
}
9.4 核心思想
设置计数器count记录当前读进程数
用count判断是否是第一个/最后一个读进程
对count的访问需要互斥
可通过额外信号量解决写进程饥饿问题
十、哲学家进餐问题
10.1 问题描述
5名哲学家围坐圆桌,交替进行思考和进餐
每两个哲学家间有一根筷子,共5根筷子
哲学家饥饿时,需要同时拿起左右两根筷子才能进餐
进餐完毕后放下筷子继续思考
10.2 错误实现(会导致死锁)
semaphore chopstick[5] = {1,1,1,1,1};Pi() { // i号哲学家进程while(1) {P(chopstick[i]); // 拿左筷子P(chopstick[(i+1)%5]); // 拿右筷子进餐;V(chopstick[i]);V(chopstick[(i+1)%5]);思考;}
}
问题:所有哲学家同时拿起左筷子,导致死锁
10.3 解决方案
10.3.1 限制哲学家数量
semaphore count = 4; // 最多允许4个哲学家同时进餐Pi() {while(1) {P(count);P(chopstick[i]);P(chopstick[(i+1)%5]);进餐;V(chopstick[i]);V(chopstick[(i+1)%5]);V(count);思考;}
}
10.3.2 奇偶号哲学家拿筷子顺序不同
奇数号:先拿左筷子,再拿右筷子
偶数号:先拿右筷子,再拿左筷子
10.3.3 同时拿起两根筷子(互斥法)
semaphore mutex = 1; // 互斥地取筷子Pi() {while(1) {P(mutex);P(chopstick[i]);P(chopstick[(i+1)%5]);V(mutex);进餐;V(chopstick[i]);V(chopstick[(i+1)%5]);思考;}
}
10.4 解决思路总结
限制数量:最多允许4个哲学家同时进餐
改变顺序:奇偶号哲学家拿筷子顺序不同
原子操作:仅当左右筷子都可用时才允许拿起
十一、管程
11.1 引入管程的原因
信号量机制问题:编写程序困难、易出错
管程目标:让程序员无需关注复杂的P、V操作
11.2 管程的定义与特征
monitor MonitorName// 1. 局部于管程的共享数据结构说明// 2. 对该数据结构进行操作的一组过程// 3. 对局部于管程的共享数据设置初始值的语句// 4. 管程有一个名字
end monitor;
基本特征:
局部于管程的数据只能被局部于管程的过程访问
进程只有通过调用管程内的过程才能进入管程访问共享数据
每次仅允许一个进程在管程内执行某个内部过程
11.3 用管程解决生产者-消费者问题
monitor ProducerConsumercondition full, empty; // 条件变量int count = 0; // 缓冲区中的产品数void insert(Item item) {if(count == N)wait(full); // 缓冲区满,等待count++;insert_item(item);if(count == 1)signal(empty); // 唤醒等待的消费者}Item remove() {if(count == 0)wait(empty); // 缓冲区空,等待count--;if(count == N-1)signal(full); // 唤醒等待的生产者return remove_item();}
end monitor;// 生产者进程
producer() {while(1) {item = 生产产品;ProducerConsumer.insert(item);}
}// 消费者进程
consumer() {while(1) {item = ProducerConsumer.remove();使用产品;}
}
11.4 管程的优势
互斥特性:由编译器实现,程序员无需关心
同步机制:通过条件变量及等待/唤醒操作实现
封装思想:提供特定的"入口"访问共享数据
11.5 Java中的类似机制
static class Monitor {private Item buffer[] = new Item[N];private int count = 0;public synchronized void insert(Item item) {// 同一时间段内只能有一个线程调用}
}
十二、知识总结与重要考点
12.1 PV操作解题思路
关系分析:找出各进程间的同步、互斥关系
整理思路:确定P、V操作的大致顺序
设置信号量:根据题目条件确定信号量初值
12.2 信号量设置原则
互斥信号量:初值一般为1
同步信号量:初值看对应资源的初始值
资源信号量:初值为资源数量
12.3 易错点与注意事项
P操作顺序:实现同步的P操作要在实现互斥的P操作之前
V操作顺序:可以交换,不会导致阻塞
信号量含义:一个信号量对应一种资源
缓冲区大小:影响是否需要互斥信号量
12.4 经典问题对比
问题类型 | 核心矛盾 | 解决关键 | 典型应用 |
---|---|---|---|
生产者-消费者 | 生产与消费速度不匹配 | 缓冲区管理 | 数据缓冲、消息队列 |
读者-写者 | 读并发与写互斥 | 读者计数器 | 文件系统、数据库 |
哲学家进餐 | 资源竞争与死锁 | 拿筷子策略 | 资源分配、死锁避免 |
12.5 复习建议
理解原理:不只是记忆代码,要理解同步互斥的本质
动手练习:多做PV操作题目,熟练掌握解题思路
对比分析:对比不同解决方案的优缺点
联系实际:理解各经典问题在实际系统中的应用
核心思维:进程同步与互斥是操作系统协调并发进程的关键机制,通过信号量、管程等工具,在保证数据一致性的同时提高系统并发度。理解各种同步问题的本质和解决方案,是掌握操作系统并发管理的核心。