# 第 31 章实验:信号量(Semaphores) 五个小程序,全部只用 POSIX 信号量(`sem_init / sem_wait / sem_post`)实现同步。 ```bash cd labs/31-semaphores && make ``` | 程序 | 对应原书 | 做什么 | |------|----------|--------| | `fork_join` | 31.3 节,Figure 31.6 | 父线程用 `sem_wait` 等子线程的 `sem_post` | | `pc` | 31.4 节,Figure 31.11 / 31.12 | 有界缓冲区;`-b` 切换到"mutex 在外层"的错误写法 | | `dining` | 31.6 节,Figure 31.15 / 31.16 | 哲学家就餐:死锁复现 + "P4 反序"修复 | | `barrier` | Homework 第 3 题 | 两个信号量 + 计数器实现屏障 | | `throttle` | 31.7 节 | 信号量初值 K 限制同时进入某区域的线程数 | ## 1. fork_join:信号量当 join 用 ```bash ./fork_join # 初值 0:parent: begin → child → parent: end ./fork_join 1 # 初值 1:父线程不会等,parent: end 先于 child 打印 ``` **观察**:子线程先睡 1 秒,父线程依然会等它。**为什么**:初值 0 表示"一开始没有东西可以给出去", 父线程的 `sem_wait` 把值减到 -1 后睡眠,直到子线程 `sem_post`。初值写成 1 就等于一开始就"送出"了一个名额,父线程直接通过。 ## 2. pc:生产者/消费者 ```bash ./pc # 正确版,1 生产者 1 消费者,MAX=1 ./pc -p 2 -c 3 -m 10 -l 100000 # 多生产者多消费者 ./pc -b # 错误版:mutex 放在 sem_wait(&full) 外面 ``` **观察**:正确版最后"生产/消费的个数和校验和一致"。错误版里程序故意让消费者先启动: 消费者拿到 `mutex` 后睡在 `full` 上,生产者再去拿 `mutex` 就睡着了——看门狗 2 秒后报告死锁并退出(返回码 2)。 **为什么**:睡在 `full` 上的消费者**仍然持有** `mutex`;能唤醒它的生产者恰恰需要这把 `mutex`,形成环。 把锁的范围缩小到只包住 `put()/get()`(Figure 31.12)就消除了这个环。 ## 3. dining:哲学家就餐 ```bash ./dining broken # 很快死锁:每人都拿着左叉等右叉 ./dining fixed # P4 先拿右叉再拿左叉,5 000 顿饭全部吃完 ./dining fixed 3000 ``` 程序在拿到第一把叉子后 `usleep(1000)`,故意放大竞争窗口,所以错误版几乎每次都会死锁。 看门狗发现 2 秒没人吃上饭就打印每位哲学家"拿着什么、在等什么",你会看到 P0→P1→…→P4→P0 的等待环。 ## 4. barrier:屏障 ```bash ./barrier 5 ``` 每个线程先打印 P1,过屏障,再打印 P2。**观察**:所有 P2 都出现在所有 P1 之后,且过屏障时"已完成 P1 的线程数"总是 N。 实现要点:`mutex` 保护计数器;最后到达的线程打开 `turnstile`;每个线程穿过后立刻 `sem_post(&turnstile)` 把门留给下一个人。 ## 5. throttle:线程节流 ```bash ./throttle 20 3 # 同时最多 3 个线程在内存密集区 ./throttle 20 0 # 不节流:20 个线程可能同时在里面 ``` **为什么**:信号量初值 = 允许同时进入的线程数("一开始愿意给出去的资源数")。 > 注意:`sem_init` 这类无名信号量在 Linux 上可用;macOS 不支持无名信号量(`sem_init` 会失败), > 在 Mac 上需要按原书 31.8 节自己用锁 + 条件变量实现一个 Zemaphore。