03. 同步与互斥
同步与互斥
基本概念
临界区问题
临界区:访问共享资源的代码段,同一时刻只能有一个进程执行。
临界区特征:
- 互斥性:同一时刻只能有一个进程在临界区内
- 有限等待:进程等待进入临界区的时间是有限的
- 空闲让进:当没有进程在临界区内时,请求进入的进程应该能够进入
临界区结构:
do {
entry_section(); // 进入区
critical_section(); // 临界区
exit_section(); // 退出区
remainder_section(); // 剩余区
} while (true);
互斥与同步
互斥:多个进程不能同时访问共享资源。
同步:多个进程按照某种顺序执行,确保操作的正确性。
区别:
- 互斥关注的是”不能同时”
- 同步关注的是”按顺序执行”
软件方法
1. 单标志法
算法描述:
- 设置一个标志位 turn
- 只有 turn 指向的进程才能进入临界区
代码实现:
int turn = 0; // 初始化为进程0
// 进程0
while (turn != 0); // 等待
critical_section();
turn = 1;
// 进程1
while (turn != 1); // 等待
critical_section();
turn = 0;
问题:强制轮流进入,违背”空闲让进”原则。
2. 双标志法
算法描述:
- 每个进程设置一个标志位
- 进程进入临界区前检查其他进程的标志
代码实现:
bool flag[2] = {false, false};
// 进程i
flag[i] = true;
while (flag[j]); // 等待其他进程
critical_section();
flag[i] = false;
问题:可能同时进入临界区,违背”互斥性”。
3. 双标志法改进
算法描述:
- 先设置自己的标志,再检查其他进程
- 如果冲突,先清除自己的标志再重试
代码实现:
bool flag[2] = {false, false};
// 进程i
flag[i] = true;
while (flag[j]) {
flag[i] = false;
delay();
flag[i] = true;
}
critical_section();
flag[i] = false;
问题:可能无限等待,违背”有限等待”原则。
4. Peterson 算法
算法描述:
- 结合 turn 标志和 flag 标志
- 满足临界区的三个要求
代码实现:
bool flag[2] = {false, false};
int turn = 0;
// 进程i
flag[i] = true;
turn = j;
while (flag[j] && turn == j);
critical_section();
flag[i] = false;
特点:
- 满足互斥性、有限等待、空闲让进
- 适用于两个进程
- 可以扩展到多个进程
硬件方法
1. 中断屏蔽
原理:在进入临界区前关闭中断,退出时恢复中断。
代码实现:
// 进入临界区
disable_interrupts();
critical_section();
// 退出临界区
enable_interrupts();
特点:
- 简单有效
- 只适用于单处理器
- 影响系统响应性
2. Test-and-Set 指令
原理:原子地测试并设置内存位置的值。
代码实现:
bool TestAndSet(bool *lock) {
bool old = *lock;
*lock = true;
return old;
}
bool lock = false;
// 进程
while (TestAndSet(&lock));
critical_section();
lock = false;
特点:
- 硬件支持,效率高
- 适用于多处理器
- 可能忙等待
3. Compare-and-Swap 指令
原理:原子地比较并交换内存位置的值。
代码实现:
int CompareAndSwap(int *value, int expected, int new_value) {
int temp = *value;
if (*value == expected)
*value = new_value;
return temp;
}
int lock = 0;
// 进程
while (CompareAndSwap(&lock, 0, 1) != 0);
critical_section();
lock = 0;
特点:
- 硬件支持,效率高
- 适用于多处理器
- 比 Test-and-Set 更灵活
信号量
基本概念
信号量:一个整型变量,除了初始化外,只能通过两个原子操作访问:
- P 操作(wait):申请资源
- V 操作(signal):释放资源
信号量类型:
- 二元信号量:值为 0 或 1,用于互斥
- 计数信号量:值可以为任意非负整数,用于同步
信号量实现
结构定义:
typedef struct {
int value;
struct process *list;
} semaphore;
P 操作实现:
void P(semaphore *S) {
S->value--;
if (S->value < 0) {
add_process_to_list(S->list);
block();
}
}
V 操作实现:
void V(semaphore *S) {
S->value++;
if (S->value <= 0) {
remove_process_from_list(S->list);
wakeup();
}
}
信号量应用
1. 互斥:
semaphore mutex = 1;
// 进程
P(&mutex);
critical_section();
V(&mutex);
2. 同步:
semaphore sync = 0;
// 进程A
statement_a();
V(&sync);
// 进程B
P(&sync);
statement_b();
3. 资源管理:
semaphore resource = N; // N个资源
// 进程
P(&resource);
use_resource();
V(&resource);
管程
基本概念
管程:一种高级同步机制,将共享变量和对它们的操作封装在一个模块中。
管程特征:
- 封装性:共享变量只能通过管程内的过程访问
- 互斥性:同一时刻只能有一个进程在管程内执行
- 条件变量:用于进程间的同步
管程结构
monitor MonitorName {
// 共享变量声明
shared variables;
// 条件变量
condition x, y;
// 过程定义
procedure P1() {
// 过程体
}
procedure P2() {
// 过程体
}
// 初始化代码
{
// 初始化代码
}
}
条件变量
操作:
wait(c)
:进程在条件变量 c 上等待signal(c)
:唤醒在条件变量 c 上等待的进程
实现:
typedef struct {
int value;
struct process *list;
} condition;
void wait(condition *c) {
c->value++;
block();
}
void signal(condition *c) {
if (c->value > 0) {
c->value--;
wakeup();
}
}
经典同步问题
1. 生产者-消费者问题
问题描述:
- 生产者进程生产数据放入缓冲区
- 消费者进程从缓冲区取出数据
- 缓冲区有限,需要同步
解决方案:
#define N 100
semaphore mutex = 1;
semaphore empty = N;
semaphore full = 0;
// 生产者
void producer() {
while (true) {
item = produce_item();
P(&empty);
P(&mutex);
insert_item(item);
V(&mutex);
V(&full);
}
}
// 消费者
void consumer() {
while (true) {
P(&full);
P(&mutex);
item = remove_item();
V(&mutex);
V(&empty);
consume_item(item);
}
}
2. 读者-写者问题
问题描述:
- 多个读者可以同时读取共享数据
- 写者必须独占访问
- 读者和写者不能同时访问
读者优先解决方案:
semaphore mutex = 1;
semaphore wrt = 1;
int readcount = 0;
// 读者
void reader() {
P(&mutex);
readcount++;
if (readcount == 1)
P(&wrt);
V(&mutex);
read_data();
P(&mutex);
readcount--;
if (readcount == 0)
V(&wrt);
V(&mutex);
}
// 写者
void writer() {
P(&wrt);
write_data();
V(&wrt);
}
3. 哲学家进餐问题
问题描述:
- 5 个哲学家围坐在圆桌旁
- 每个哲学家需要两根筷子才能进餐
- 筷子放在哲学家之间
解决方案:
semaphore chopstick[5] = {1, 1, 1, 1, 1};
semaphore mutex = 1;
void philosopher(int i) {
while (true) {
think();
P(&mutex);
P(&chopstick[i]);
P(&chopstick[(i+1)%5]);
V(&mutex);
eat();
V(&chopstick[i]);
V(&chopstick[(i+1)%5]);
}
}
死锁问题
死锁定义
死锁:多个进程因竞争资源而造成的一种互相等待的现象。
死锁条件
四个必要条件:
- 互斥条件:资源不能被多个进程同时使用
- 请求和保持条件:进程在等待其他资源时,不释放已占有的资源
- 不可剥夺条件:资源不能被强制剥夺
- 循环等待条件:存在进程等待链,形成循环
死锁预防
破坏互斥条件:
- 使用可共享的资源
- 使用虚拟化技术
破坏请求和保持条件:
- 一次性分配所有资源
- 资源预分配策略
破坏不可剥夺条件:
- 允许资源抢占
- 设置资源优先级
破坏循环等待条件:
- 资源有序分配
- 资源编号策略
死锁避免
银行家算法:
- 检查资源分配是否安全
- 只有在安全状态下才分配资源
安全状态检查:
bool is_safe_state() {
int work[RESOURCES];
bool finish[PROCESSES];
// 初始化
for (int i = 0; i < RESOURCES; i++)
work[i] = available[i];
for (int i = 0; i < PROCESSES; i++)
finish[i] = false;
// 查找可以完成的进程
for (int count = 0; count < PROCESSES; count++) {
bool found = false;
for (int i = 0; i < PROCESSES; i++) {
if (!finish[i] && can_allocate(i, work)) {
for (int j = 0; j < RESOURCES; j++)
work[j] += allocation[i][j];
finish[i] = true;
found = true;
}
}
if (!found) return false;
}
return true;
}
死锁检测
资源分配图:
- 节点表示进程和资源
- 边表示资源分配和请求关系
- 检测图中是否存在环路
检测算法:
bool detect_deadlock() {
// 简化资源分配图
while (存在可以简化的节点) {
简化节点;
}
// 检查是否还有边
if (图中还有边)
return true; // 存在死锁
else
return false; // 不存在死锁
}
死锁恢复
进程终止:
- 终止所有死锁进程
- 逐个终止死锁进程
资源抢占:
- 选择牺牲进程
- 回滚到安全状态
- 重新分配资源