Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

自旋锁与睡眠锁

复习

  • 程序竞争:多条执行线同时读写共享数据,可能互相干扰
  • 临界区与原子操作:哪些代码必须表现得像不可分割的一步
  • 互斥锁:让同一时刻只有一个执行线进入临界区

TL;DR

  • 拿不到锁时,有两种等法:原地重试(自旋)睡下等待(睡眠)
  • 自旋锁:一直占着 CPU 重试,适合临界区很短的情况
  • 睡眠锁:让出 CPU、等别人唤醒,适合等待较久的情况
  • 选哪种,取决于预计要等多久

正文

  锁的接口看似简单:拿不到就等。可“怎么等”,却大有讲究。同样是在门口等锁,有两种截然不同的姿态。

自旋:原地打转

  自旋锁(spinlock)的做法是:

while (拿不到锁) {
    // 什么都不做,继续试
}

  它一直占着 CPU,反复检查锁有没有被放开。好处是:

  • 不涉及上下文切换,反应极快
  • 一旦锁释放,几乎是立刻就抢到

  代价也很明显:等待期间它一直在空转烧 CPU。如果临界区很短,等一小会儿就轮到自己,这很划算;可如果需要等很久,那就是白白浪费。

睡眠:让出 CPU

  睡眠锁(sleeping lock)则温和得多:拿不到锁时,直接把自己标记为阻塞,交给内核,让出 CPU 去干别的;等锁被释放时,再由内核把它唤醒。

  这样一来,等待期间 CPU 可以去跑别的线程,不浪费。但代价是:睡眠和唤醒都要做上下文切换,开销比自旋大。

怎么选

  两种等法,正好对应两种场景:

自旋锁睡眠锁
等待方式原地重试,不让 CPU让出 CPU,等唤醒
开销不切换,但空转要切换
适合临界区很短等待较久

  一句话:预计很快能拿到,就自旋;预计要等很久,就睡眠。

  还有一个细节:在单核机器上,如果拿着锁的线程根本没法运行(比如它被挂起了),那么自旋的线程永远也等不到锁释放,只会一直空转;这时睡眠锁才靠谱。所以自旋锁多用在多核、且临界区极短的内核场景里。

思考题

  如果一个线程要等锁接近一秒钟,用自旋锁会发生什么?这一秒钟里,CPU 在做什么“有用的事”吗?

小结

知识点

  • 自旋锁:忙等,不切换,适合短临界区
  • 睡眠锁:阻塞并让出 CPU,适合长等待
  • 选择的依据是预计等待时间
  • 单核且锁可能长时间不可用时应避免自旋

参考资料

  1. Wikipedia(zh):自旋锁:spinlock
  2. Wikipedia(zh):信号量:睡眠与唤醒

思考题答案(仅供参考)

  用自旋锁等接近一秒,意味着这个线程会占着 CPU 原地空转近一秒,反复检查一个暂时不会变的状态。这一秒里它没有做任何有用的事,纯粹浪费了一个 CPU 核心——如果别的可运行线程还排着队,就更亏。这种情况明显应该用睡眠锁:把自己挂起,让出 CPU,等锁释放时再被唤醒。所以“自旋适合短等、睡眠适合长等”,本质上是在“切换开销”和“空转浪费”之间选成本更低的那个。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪