回顾:
- 使用信号量的并发同步。
- 并发编程的生产者/消费者问题和困难。
本章小结:
- 继续看有界缓冲区问题
- 用餐哲学家(dining philosophers)的问题
哲学家就餐问题:
一个著名的计算机科学问题,用于解释同步、死锁和资源竞争等并发问题。
生产者/消费者问题
单一生产者/消费者和无界缓冲区
竞争条件(不存在的元素=>items= -1)
两个信号量:
-
sync——二进制信号量(互斥),保护对items的读写; -
delay_consumer——用来让消费者在没有元素时睡眠,有元素时被唤醒。
| 内容 | 含义 |
|---|---|
void * consumer(void *p) / void * producer(void *p) | 两个线程函数的声明。 |
| 1 → 0 → 1 | 表示 items 随时间变化的示例值:最初为 1,消费者拿走一个后变成 0,生产者再生产一个又变成 1。 |
sem_wait(&delay_consumer);(消费者开头) |
若
|
sem_wait(&sync); / sem_post(&sync); | 进入/退出临界区,对 items 做互斥保护,防止并发读写。 |
items-- / items++ |
消费一个 / 生产一个。 消费者拿走商品 / 生产者放入商品。 |
0 => 1(写在 items++ 旁) | 示意:当 items 从 0 变到 1 时,生产者执行 sem_post(&delay_consumer) 唤醒等待中的消费者。 |
if(items == 1)(生产者) | 当 items 刚刚由 0 变成 1,说明之前没货,现在需要唤醒消费者。 |
if(items == 0)(消费者) | 当 items 变成 0,消费者会再次执行 sem_wait(&delay_consumer) 进入睡眠。 |
| Race condition non-existing element => items = -1 | 错误场景:如果对 items 的读写不加 sync 保护,两个线程并发执行就可能出现 items 变成负数(试图消费一个不存在的元素)。强调了必须加锁。 |
虽然我们同步了对items的访问,但这并不能防止涉及该变量的竞争条件!
我们没有保留items和信号量delay_consumer之间的关系。
死锁
当 items 已经为 0 时,再次 sem_wait(&delay_consumer);
而此时信号量很可能已经是 0,消费者再次阻塞,生产者却因为锁名错误永远无法唤醒它 → 又一种死锁场景。
解决方案
使用临时变量:
- 复制临界区内items的值
- 递减delay_consumer信号量以使其一致
多生产者,多消费者,有限缓冲
前面的代码(一个消费者,一个生产者)通过存储 items 的值来工作
该问题的另一种变体有n个消费者,m个生产者和一个固定的缓冲区大小N。解决方案基于3个信号量:
sync:用于对缓冲区强制互斥empty:跟踪空缓冲区的数量,初始化为Nfull:跟踪已满缓冲区的数量,初始化为0
empty和full是计数信号量,代表资源
例子:多生产者 / 多消费者、固定大小 N = 3 的循环缓冲区


/* 全局 */
int items = 0; // 当前缓冲区里元素个数
sem_t empty = 3; // 空位
sem_t full = 0; // 数据
sem_t sync = 1; // 互斥锁
/* 任一生产者线程 */
void *producer(void *a)
{
while (1) {
sem_wait(&empty); // 等空位
sem_wait(&sync); // 进临界区
items++; // 放入 1 个
printf("Producer:%d\n", items);
sem_post(&sync); // 出临界区
sem_post(&full); // 增加可读计数
}
}
/* 任一消费者线程 */
void *consumer(void *a)
{
while (1) {
sem_wait(&full); // 等数据
sem_wait(&sync); // 进临界区
items--; // 取走 1 个
printf("Consumer:%d\n", items);
sem_post(&sync); // 出临界区
sem_post(&empty); // 增加空位
}
}
3个信号量:
sync:二进制互斥锁,保护对共享变量items的读写,初始化为1empty:当前缓冲区还剩余多少个空位可写,初始化为3full:当前缓冲区已有多少个数据可读,初始化为0
哲学家进餐问题
描述

问题定义为:
- 五位哲学家坐在一张圆桌上
- 每个人都有一盘意大利面
- 意大利面太滑了,每个哲学家需要2把叉子才能吃
- 当饥饿/hungry的时候(在思考/thinking的间隙),哲学家试图获得他们左右两边的叉子
这反映了在多个进程(哲学家)之间共享有限资源(叉子)的一般问题。
方案1:死锁
叉子(Forks)由信号量表示(初始化为1)
- 如果叉子可用,则为1:哲学家可以继续
- 如果叉子不可用,则为0;如果试图获取它,则进入睡眠状态
第一种方法:朴素解法,一个会死锁的版本。每个哲学家拿起一把叉子,等待第二把叉子变得可用(不放下第一把)。
#define N 5
sem_t forks[N];
void * philosopher(void * id) {
int i = *((int *) id);
int left = (i + N - 1) % N;
int right = i % N;
while(1) {
printf("%d is thinking\n", i);
printf("%d is hungry\n", i);
sem_wait(&forks[left]);
sem_wait(&forks[right]);
printf("%d is eating\n", i);
sem_post(&forks[left]);
sem_post(&forks[right]);
}
}
1. 全局定义
#define N 5
sem_t forks[N];
-
一共 5 位哲学家,编号 0‥4。
-
每位哲学家左右各有一把叉子,共 5 把。
-
信号量
forks[i]的初值为 1,表示第 i 把叉子可用。
2. 哲学家线程函数
void *philosopher(void *id) {
int i = *((int *)id); // 哲学家编号
int left = (i + N - 1) % N; // 左手叉子编号
int right = i % N; // 右手叉子编号
3. 无限循环:思考 → 饥饿 → 拿叉 → 吃 → 放叉
while (1) {
printf("%d is thinking\n", i); // 思考
printf("%d is hungry\n", i); // 饥饿
sem_wait(&forks[left]); // 拿左叉
sem_wait(&forks[right]); // 拿右叉
printf("%d is eating\n", i); // 吃面
sem_post(&forks[left]); // 放左叉
sem_post(&forks[right]); // 放右叉
}
}
4. 为什么会死锁?
如果 5 位哲学家同时先拿左边的叉子,再拿右边的叉子(顺序相同),就会出现:
哲学家 0 拿到叉子 4
哲学家 1 拿到叉子 0
哲学家 2 拿到叉子 1
哲学家 3 拿到叉子 2
哲学家 4 拿到叉子 3
此时每人都持有一把叉子,都在等待另一把,循环等待 → 死锁。

可以通过以下方式防止死锁:
- 把分叉放下,等待一个随机的时间——潜在的其他bug。
- 在桌子上多放一个叉子——回答一个更简单的问题!
- 一个由哲学家设定的全局互斥锁,当他们想吃东西的时候,一次只能有一个人吃。——不会导致最大的并行性,因为一次只有一个吃。
方案2:全局互斥信号量
sem_t eating; // 全局互斥信号量,初值为 4(或 1,见下方说明)
void * philosopher(void * id)
{
int i = (int) id;
int left = (i + N - 1) % N;
int right = i % N;
while(1)
{
printf("%d is thinking\n", i);
printf("%d is hungry\n", i);
sem_wait(&eating); /**** mutex/semaphore ****/ // ① 开始干饭
sem_wait(&forks[left]); // ② 拿左叉
sem_wait(&forks[right]); // ③ 拿右叉
printf("%d is eating\n", i);
sem_post(&forks[left]); // ④ 放左叉
sem_post(&forks[right]); // ⑤ 放右叉
sem_post(&eating); /**** mutex/semaphore ****/ // ⑥ 停止干饭
}
}
| 内容 | 含义 |
|---|---|
sem_t eating | 全局互斥信号量,用来限制同时尝试拿叉子的人数。 |
sem_wait(&eating); / sem_post(&eating); | 进入/退出“临界区域(拿叉→吃→放叉)”。 |
夹在 eating 之间的三行 | 拿两把叉、吃、再放叉,被全局信号量包住,保证同一时刻最多只有 N-1=4 位哲学家能同时伸手,从而破坏循环等待条件,天然避免死锁。 |
注释 /**** mutex/semaphore ****/ | 强调这一段用互斥信号量(mutex 或 binary semaphore)保护。 |
为什么能防死锁?
-
最多只允许 4 人同时进入“吃饭区”(
eating初值设为 4)。 -
因此至少有一位哲学家无法同时拿叉子,循环等待链被切断 ⇒ 死锁不可能出现。

理解
1. 可以将eating信号量的值初始化为2来创建更多的并行性吗?
答:可以把 eating 信号量的初值设为 2,这样最多允许 2 位哲学家同时进入“拿叉-吃饭-放叉”的临界区,确实能提高并发度(相比初值 1)。不过:
-
并发度虽提高,但仍低于 N-1(4) 的最大安全值,理论上仍不会死锁。
-
实际吞吐量可能反而下降,因为同一时刻只能有 2 人在吃饭,CPU/叉子利用率降低。
-
如果目的只是“防死锁”,N-1 是最小且最有效的限制;设成 2 只是折中,不是最优。
→ 可以设成 2,但意义不大;通常直接设为 N-1。
2. 对于单生产者消费者无界缓冲问题,你能找到一个更好的解决方案吗?
答:不需要再维护 empty/full 两个计数信号量,因为缓冲区无限大,永远不会“满”。
• 只需 一把互斥锁(或一条原子链)保护对 in/out 指针的读写;
• 再加 一个条件变量 / 信号量 data_avail(初值 0)告诉“消费者现在至少有一条数据”。
伪代码:避免了 “空/满” 的管理开销。
mutex mtx;
cond data_avail;
int in = 0, out = 0;
item buffer[];
producer() {
while (true) {
item = produce();
lock(&mtx);
buffer[in] = item;
in = (in + 1) % INF; // INF 足够大
unlock(&mtx);
signal(&data_avail);
}
}
consumer() {
while (true) {
wait(&data_avail);
lock(&mtx);
item = buffer[out];
out = (out + 1) % INF;
unlock(&mtx);
consume(item);
}
}
3. 2×N 位哲学家围成一圈,最多同时几人吃饭?
答:每把叉子被相邻两人共享;任意两人不能共享同一把叉子。
把叉子看作图的边,哲学家看作顶点。最大独立集大小为 N。
因此 最多 N 位 哲学家能同时吃饭。
4. 2×N+1 位哲学家围成一圈,最多同时几人吃饭?
答:总人数为奇数,同理每两人之间仍共享一把叉子。
最大独立集大小为 N。
因此 最多 N 位 哲学家能同时吃饭。
899

被折叠的 条评论
为什么被折叠?



