Skip to content

第四章 并发控制:同步、互斥与死锁


1. 并发问题的根源:竞态条件

第二章说过,线程的卖点是共享内存。这一章讲共享的代价。

看一个最小的例子:两个线程都执行 count++,count 初值为 0,直觉上结果应该是 2。但 count++ 在 CPU 上不是一步完成的,它是三条指令:

  1. 把 count 从内存读进寄存器(读)
  2. 寄存器加 1(改)
  3. 把寄存器写回内存(写)

如果线程 A 刚执行完第 1 步,时钟中断来了,切换到线程 B 把三步全做完(count 变 1),再切回 A——A 的寄存器里还是旧值 0,加 1 后写回,count 最终是 1 而不是 2。一次加法凭空丢了。

这种"结果取决于线程恰好怎样交错"的现象叫竞态条件(race condition,白话:谁跑得巧谁说了算,结果像抽奖)。它的可怕之处在于不确定性:一万次运行可能九千九百次正确,专挑演示的时候出错。

根源总结成一句话:共享数据 + 至少一方在写 + 操作不是原子的(原子:要么整个做完、要么完全没做,中间不可能被打断),三者同时成立就会出竞态。所有并发控制手段,本质都是破坏这三条中的某一条。

2. 临界区与四个准则

把"访问共享资源的那段代码"称为临界区(critical section)。上例中 count++ 的三条指令就是临界区。解决竞态的思路是:保证任一时刻至多一个线程在临界区内,这个要求叫互斥

顺带区分一对高频概念:

  • 互斥:多方抢同一资源,谁先谁后无所谓,但不能同时。是一种"间接制约"。
  • 同步:多方协作有先后依赖,如"B 必须等 A 产出数据后才能加工"。是一种"直接制约"。

任何一个合格的临界区方案必须满足四个准则:

准则白话解释
空闲让进没人在临界区时,想进的人应能立即进,不许故意空着
忙则等待有人在临界区时,其他人必须等
有限等待等待的人在有限时间内必须能进去,不能饿死
让权等待进不去就应让出 CPU,别占着 CPU 空转干等

前两条保证正确性,第三条保证公平性,第四条保证效率(让权等待是"应当"而非"必须",自旋锁就不满足它,但仍算可用方案)。

3. 互斥的实现层次:从关中断到管程

4. 信号量与 PV 操作

5. 经典问题一:生产者-消费者

6. 经典问题二:读者-写者

7. 死锁:四个必要条件

8. 死锁处理的四条路线

9. 银行家算法手算例题

10. 本章要点回顾

11. 做题提醒