# 第 30 章实验:条件变量 三个小程序,对应原书第 30 章的三个核心例子。都用 `gcc -pthread` 编译,在 Linux / WSL2 上测试过。 ```bash make # 生成 pc、join、alloc make clean ``` ## 1. pc:生产者/消费者的三个版本 ```bash ./pc -v 1 -p 1 -c 2 -m 1 -l 100000 # if + 单 CV(Figure 30.8) ./pc -v 2 -p 1 -c 2 -m 1 -l 100000 # while + 单 CV(Figure 30.10) ./pc -v 3 -p 1 -c 2 -m 1 -l 100000 # while + empty/fill(Figure 30.14,正确) ./pc -v 3 -p 1 -c 2 -m 10 -l 100000 # 缓冲区变大 ./pc -v 1 -p 1 -c 1 -m 1 -l 100000 # 单生产者单消费者:v1 也能跑完 ``` 参数:`-v` 版本、`-p` 生产者数、`-c` 消费者数、`-m` 缓冲区大小 MAX、`-l` 每个生产者要生产的数据个数。 **应该看到什么:** - **v1 + 两个消费者**:几乎每次都报 `断言失败:get() 时缓冲区为空`。这就是 Figure 30.9:被唤醒的消费者还没运行,另一个消费者就插队把数据取走了;而 if 不会重新检查条件。 - **v2 + 两个消费者**:通常只消费几个数据就停住了。看门狗线程发现 2 秒内没有任何 put/get,报告"所有线程都睡着了"。这就是 Figure 30.11:消费者的 signal 叫醒了另一个消费者,生产者没人叫。 - **v3**:总能跑完,但仍然会统计到一些"白醒",也就是被唤醒后发现条件仍不满足。这是 Mesa 语义下的正常现象,while 循环会处理它。 - **MAX 从 1 增加到 10**:同样的工作量会快很多,因为上下文切换少了。 - **单生产者单消费者**:v1 和 v2 都能跑完,与原书的分析一致。 ## 2. join:用条件变量等待子线程 ```bash ./join 0 # 正确版:状态变量 done + 锁 + while(Figure 30.3) ./join 1 # 没有状态变量(Figure 30.4),子线程先跑完 → 信号丢失 ./join 2 # 不持锁(Figure 30.5),在"检查"和"wait"之间制造窗口 → 信号丢失 ``` 错误版本用 `pthread_cond_timedwait` 最多等 2 秒,然后打印"信号丢失"。真实程序里,父线程会永远睡下去。 ## 3. alloc:覆盖条件(Figure 30.15) ```bash ./alloc signal # free(50) 只叫醒一个线程:可能叫醒要 100 字节的 Ta,而要 10 字节的 Tb 被遗忘 ./alloc broadcast # 叫醒所有线程:Tb 分配成功,Ta 白醒一次后继续睡 ``` 注意:Linux 并不保证 signal 唤醒哪一个等待者,所以 `./alloc signal` 在你的机器上也可能碰巧叫醒 Tb。正因为不确定,这里才需要 broadcast。 ## 为什么会这样 - 条件变量没有记忆:没人在等时,signal 什么也不做。所以必须配合状态变量(done、count、bytesLeft),并且在持锁时检查和修改它。 - Mesa 语义:被唤醒只是进入就绪队列,真正运行时状态可能已经变了,所以永远用 while 重新检查。 - 一个条件变量上如果睡着两类等待者,signal 可能叫错人。所以要让不同的条件对应不同的条件变量(empty/fill);实在无法判断该叫谁时,才用 broadcast。