跳到主要内容
约 3 分钟2 张卡

线程同步

线程同步协调共享状态访问与执行顺序。本文负责互斥锁、条件变量、信号量和自旋等待。

1. 面试核心​

先记住这几条,再读正文
  • 读改写原子性。 读改写需要完整的原子性,独立读取和写入会造成丢失更新。
  • 同步范围。 共享不变量的所有访问都要遵守同一套同步规则。
  • 条件等待。 条件变量 wait 原子释放锁并等待,返回前重新获取锁。卡 · 面试核心
  • 通知与许可。 条件变量唤醒后需循环检查谓词,信号量则保存许可计数。卡 · 条件变量

2. 临界区与共享不变量​

余额初始为 100。两个线程各加 10,若都先读到 100,就都写回 110。最终丢失一次更新。

应把读取、计算和写回放在同一临界区。单个计数器也可使用合适的原子读改写操作。多变量约束则需保证整组操作一起成立。

转账同时修改两个账户。观察者也要遵守同一同步规则,避免读到中间状态。仅保护写入而放任读者访问,会留下数据竞争。

3. 条件变量​

以下是条件变量的伪代码:

lock(mutex)
while queue is empty:
wait(notEmpty, mutex)
item = queue.removeFirst()
unlock(mutex)

wait 原子地释放 mutex 并进入等待。返回前,它重新获取 mutex。生产者在相同互斥规则下入队,再通知等待者。

消费者用 while 检查队列。虚假唤醒会使线程在没有新元素时返回。其他消费者也能在当前线程获锁前取走元素。因此唤醒后要重新检查谓词。

存在等待者时,signal 至少唤醒一个。broadcast 则唤醒全部等待者。被唤醒线程仍需竞争 mutex。通知时没有等待者,条件变量不会保存这次通知。

4. 信号量与许可​

信号量保存非负许可计数。sem_wait 获取一份许可,计数为 0 时等待。sem_post 归还许可,也可由另一线程执行。

例如并发任务上限为 8,可用初值为 8 的信号量。任务取得许可后进入,退出时归还许可。计数限制容量,业务对象仍需自己的并发保护。

互斥锁具有持有者语义,应由持有线程解锁。信号量表达资源数量或通知计数。二者应按共享状态的要求选择。

5. 自旋与阻塞等待​

自旋线程持续检查锁状态。持有时间短且持有者正在运行时,自旋能减少调度成本。临界区较长或持有者被抢占时,自旋会空耗 CPU。

阻塞等待让线程休眠,并在可用时唤醒。它节省等待期间的 CPU,但增加调度开销。读多写少且临界区足够长时,还可评估读写锁。读写锁的公平性策略会影响写者等待。

6. 使用边界​

同步既协调执行,也要满足数据可见性规则。C/C++ volatile 本身不提供线程同步。具体语言需使用对应的锁与原子机制。

持锁执行慢网络调用会扩大临界区等待。锁顺序和失败释放策略见死锁。

7 · 面试题卡片​

先想答案,再翻面先在心里答一遍,再点开卡片对照参考回答。卡片之间可以左右切换,看完会回到这里。
逐张翻看 2 张 →
未标记的卡读完后明天进入复习;已标记的按状态安排
本页题目与参考回答

7.1 线程间有哪些同步方式,互斥锁和条件变量分别解决什么问题?​

考察重点:互斥访问、条件等待与许可数量的区别,以及唤醒后的重新检查。

相关内容:面试核心、临界区与共享不变量、使用边界。

参考回答

互斥锁让临界区互斥执行,并建立相应的可见性。

条件变量让线程释放锁并等待条件变化。唤醒后重新持锁检查谓词。信号量保存许可计数,适合控制有限资源。

7.2 条件变量的通知为什么不能当作信号量许可使用?​

考察重点:谓词、丢失通知和许可计数。

相关内容:条件变量、信号量与许可。

参考回答

条件变量只通知等待者检查共享状态。

通知时没有等待者,通知不会保留下来。信号量则保存许可计数,后续 wait 可以消耗它。条件变量必须配合互斥锁和谓词循环。

8. 参考资料​