自旋锁与睡眠锁
复习
- 程序竞争:多条执行线同时读写共享数据,可能互相干扰
- 临界区与原子操作:哪些代码必须表现得像不可分割的一步
- 互斥锁:让同一时刻只有一个执行线进入临界区
TL;DR
- 拿不到锁时,有两种等法:原地重试(自旋) 或 睡下等待(睡眠)
- 自旋锁:一直占着 CPU 重试,适合临界区很短的情况
- 睡眠锁:让出 CPU、等别人唤醒,适合等待较久的情况
- 选哪种,取决于预计要等多久
正文
锁的接口看似简单:拿不到就等。可“怎么等”,却大有讲究。同样是在门口等锁,有两种截然不同的姿态。
自旋:原地打转
自旋锁(spinlock)的做法是:
while (拿不到锁) {
// 什么都不做,继续试
}
它一直占着 CPU,反复检查锁有没有被放开。好处是:
- 不涉及上下文切换,反应极快
- 一旦锁释放,几乎是立刻就抢到
代价也很明显:等待期间它一直在空转烧 CPU。如果临界区很短,等一小会儿就轮到自己,这很划算;可如果需要等很久,那就是白白浪费。
睡眠:让出 CPU
睡眠锁(sleeping lock)则温和得多:拿不到锁时,直接把自己标记为阻塞,交给内核,让出 CPU 去干别的;等锁被释放时,再由内核把它唤醒。
这样一来,等待期间 CPU 可以去跑别的线程,不浪费。但代价是:睡眠和唤醒都要做上下文切换,开销比自旋大。
怎么选
两种等法,正好对应两种场景:
| 自旋锁 | 睡眠锁 | |
|---|---|---|
| 等待方式 | 原地重试,不让 CPU | 让出 CPU,等唤醒 |
| 开销 | 不切换,但空转 | 要切换 |
| 适合 | 临界区很短 | 等待较久 |
一句话:预计很快能拿到,就自旋;预计要等很久,就睡眠。
还有一个细节:在单核机器上,如果拿着锁的线程根本没法运行(比如它被挂起了),那么自旋的线程永远也等不到锁释放,只会一直空转;这时睡眠锁才靠谱。所以自旋锁多用在多核、且临界区极短的内核场景里。
思考题
如果一个线程要等锁接近一秒钟,用自旋锁会发生什么?这一秒钟里,CPU 在做什么“有用的事”吗?
小结
知识点
- 自旋锁:忙等,不切换,适合短临界区
- 睡眠锁:阻塞并让出 CPU,适合长等待
- 选择的依据是预计等待时间
- 单核且锁可能长时间不可用时应避免自旋
参考资料
- Wikipedia(zh):自旋锁:spinlock
- Wikipedia(zh):信号量:睡眠与唤醒
思考题答案(仅供参考)
用自旋锁等接近一秒,意味着这个线程会占着 CPU 原地空转近一秒,反复检查一个暂时不会变的状态。这一秒里它没有做任何有用的事,纯粹浪费了一个 CPU 核心——如果别的可运行线程还排着队,就更亏。这种情况明显应该用睡眠锁:把自己挂起,让出 CPU,等锁释放时再被唤醒。所以“自旋适合短等、睡眠适合长等”,本质上是在“切换开销”和“空转浪费”之间选成本更低的那个。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪