主题
第四章 并发控制:同步、互斥与死锁
1. 并发问题的根源:竞态条件
第二章说过,线程的卖点是共享内存。这一章讲共享的代价。
看一个最小的例子:两个线程都执行 count++,count 初值为 0,直觉上结果应该是 2。但 count++ 在 CPU 上不是一步完成的,它是三条指令:
- 把 count 从内存读进寄存器(读)
- 寄存器加 1(改)
- 把寄存器写回内存(写)
如果线程 A 刚执行完第 1 步,时钟中断来了,切换到线程 B 把三步全做完(count 变 1),再切回 A——A 的寄存器里还是旧值 0,加 1 后写回,count 最终是 1 而不是 2。一次加法凭空丢了。
这种"结果取决于线程恰好怎样交错"的现象叫竞态条件(race condition,白话:谁跑得巧谁说了算,结果像抽奖)。它的可怕之处在于不确定性:一万次运行可能九千九百次正确,专挑演示的时候出错。
根源总结成一句话:共享数据 + 至少一方在写 + 操作不是原子的(原子:要么整个做完、要么完全没做,中间不可能被打断),三者同时成立就会出竞态。所有并发控制手段,本质都是破坏这三条中的某一条。
2. 临界区与四个准则
把"访问共享资源的那段代码"称为临界区(critical section)。上例中 count++ 的三条指令就是临界区。解决竞态的思路是:保证任一时刻至多一个线程在临界区内,这个要求叫互斥。
顺带区分一对高频概念:
- 互斥:多方抢同一资源,谁先谁后无所谓,但不能同时。是一种"间接制约"。
- 同步:多方协作有先后依赖,如"B 必须等 A 产出数据后才能加工"。是一种"直接制约"。
任何一个合格的临界区方案必须满足四个准则:
| 准则 | 白话解释 |
|---|---|
| 空闲让进 | 没人在临界区时,想进的人应能立即进,不许故意空着 |
| 忙则等待 | 有人在临界区时,其他人必须等 |
| 有限等待 | 等待的人在有限时间内必须能进去,不能饿死 |
| 让权等待 | 进不去就应让出 CPU,别占着 CPU 空转干等 |
前两条保证正确性,第三条保证公平性,第四条保证效率(让权等待是"应当"而非"必须",自旋锁就不满足它,但仍算可用方案)。
