经典同步问题详解
1. 生产者-消费者问题 🏭🛒
问题描述:
- 生产者生产产品放入缓冲区,消费者从缓冲区取出产品。
- 缓冲区规则:
- 缓冲区满时,生产者等待(
empty=0
)。 - 缓冲区空时,消费者等待(
full=0
)。 - 访问缓冲区必须互斥(防止数据竞争)。
- 缓冲区满时,生产者等待(
关键点:
✅ 同步关系:
- 生产者
V(full)
通知消费者“有产品可消费”。 - 消费者
V(empty)
通知生产者“有空位可生产”。
✅ 互斥关系:
- 通过
mutex
保证同一时间只有一个进程操作缓冲区。
⚠️ 死锁风险:
- 若先
P(mutex)
再P(empty)
,可能所有进程互相等待(如缓冲区满时生产者占mutex
等empty
,消费者又等mutex
)。 - 正确顺序:先检查资源(
empty/full
),再锁mutex
。
代码实现:
semaphore mutex = 1; // 互斥锁,保护缓冲区
semaphore empty = n; // 空缓冲区数量(初始=n)
semaphore full = 0; // 满缓冲区数量(初始=0)void producer() {while (1) {生产一个产品;P(empty); // 申请一个空位(如果没有空位,阻塞)P(mutex); // 进入临界区(锁住缓冲区)把产品放入缓冲区;V(mutex); // 退出临界区(释放锁)V(full); // 增加一个满缓冲区(通知消费者)}
}void consumer() {while (1) {P(full); // 申请一个产品(如果没有产品,阻塞)P(mutex); // 进入临界区(锁住缓冲区)从缓冲区取出产品;V(mutex); // 退出临界区(释放锁)V(empty); // 增加一个空位(通知生产者)消费产品;}
}
关键点 🔑
P(empty)
和P(full)
必须在P(mutex)
之前- 如果先
P(mutex)
再P(empty)
,可能导致死锁(生产者占着锁等空位,消费者进不去)。
- 如果先
V(full)
和V(empty)
是“通知”对方V(full)
告诉消费者“有数据可读”。V(empty)
告诉生产者“有空位可写”。
2. 读者-写者问题 📖✍️
问题描述:
- 读者可共享读取文件,写者必须独占写入。
- 要求:
- 读时允许其他读者,但禁止写者。
- 写时禁止其他所有读写操作。
关键点:
🔹 读者优先(默认方案):
- 第一个读者锁
rw
(阻止写者),最后一个读者释放rw
。 count
记录当前读者数,用mutex
保护count
。
🔹 写者优先(公平方案):
- 新增信号量
w
,写者来时阻止新读者排队。 - 已有读者读完后再唤醒写者。
⚠️ 问题:
- 默认方案可能导致写者饥饿(读者源源不断时写者永远等待)。
代码实现(读者优先)
int read_count = 0; // 当前读者数量
semaphore rw_mutex = 1; // 读写锁(写者独占)
semaphore count_mutex = 1; // 保护 read_countvoid writer() {while (1) {P(rw_mutex); // 写者独占访问写入文件;V(rw_mutex); // 释放锁}
}void reader() {while (1) {P(count_mutex); // 保护 read_countif (read_count == 0) P(rw_mutex); // 第一个读者锁住写者read_count++;V(count_mutex);读取文件; // 多个读者可同时读P(count_mutex);read_count--;if (read_count == 0)V(rw_mutex); // 最后一个读者释放锁V(count_mutex);}
}
关键点 🔑
- 读者优先:只要有一个读者在读,后续读者可以直接进入,写者必须等待。
read_count
需要互斥保护,否则多个读者同时修改会导致竞争。- 写者必须独占
rw_mutex
,保证写操作安全。
3. 哲学家问题 🍜🥢
问题描述:
- 5哲学家围坐,每人需左右两根筷子吃饭。
- 死锁场景:若所有人同时拿左筷子,则无人能拿到右筷子,永远等待。
解决方案:
🔸 破坏死锁条件:
- 限制并发:最多4人同时拿筷子(至少1人能吃到)。
- 原子拿筷:只有左右筷子都空闲时才拿(用
mutex
保护拿筷操作)。 - 奇偶策略:奇数号先拿左筷,偶数号先拿右筷。
核心思想:
- 避免“贪心”行为(拿一根等一根),确保要么拿到全部资源,要么不拿。
代码实现(避免死锁版)
semaphore chopstick[5] = {1, 1, 1, 1, 1}; // 5根筷子
semaphore mutex = 1; // 保护拿筷子操作void philosopher(int i) {while (1) {P(mutex); // 进入临界区P(chopstick[i]); // 拿左筷子P(chopstick[(i+1)%5]); // 拿右筷子V(mutex); // 离开临界区吃饭; // 同时持有两根筷子V(chopstick[i]); // 放回左筷子V(chopstick[(i+1)%5]); // 放回右筷子}
}
关键点 🔑
- 直接拿两根筷子可能死锁(所有人同时拿左筷子,互相等待右筷子)。
- 用
mutex
保证拿筷子是原子操作,避免竞争。 - 替代方案:
- 限制最多 4 个哲学家同时拿筷子。
- 奇数号先拿左筷子,偶数号先拿右筷子。
总结 🌟
- 生产者-消费者问题:通过empty/full信号量实现缓冲区的同步控制,mutex保证互斥访问,注意P操作顺序避免死锁;
- 读者-写者问题:使用计数器记录读者数量,读写锁保证写者独占,默认方案可能导致写者饥饿;
- 哲学家问题:采用原子化拿筷策略或限制并发人数打破循环等待。这些案例揭示了并发编程的核心:资源同步、互斥访问与死锁预防的平衡艺术
问题 | 核心矛盾 | 关键解法 |
生产者-消费者 | 缓冲区同步+互斥 |
|
读者-写者 | 读写互斥 vs 读读共享 |
|
哲学家 | 循环等待死锁 | 资源预判/拿筷策略 |
理解这些问题的核心是:同步协作 + 互斥保护 + 避免死锁! 🚀