Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

信号量

本章为进阶内容,零基础读者可以跳过,不影响后续阅读。

复习

  • 自旋锁与睡眠锁:等待锁时应该原地重试还是让出 CPU
  • 等待与通知:条件变量如何让线程在条件不满足时睡眠
  • 生产者与消费者:用互斥和等待通知串起一个完整例子

TL;DR

  • 信号量(semaphore)是一个带计数的同步工具
  • P 操作把计数减一,若为负就等待;V 操作把计数加一,并唤醒等待者
  • 计数为 1 时,它可以当互斥锁用
  • 计数大于 1 时,它可以管理“有 N 份的有限资源”

正文

  上一章用“锁 + 条件变量”实现了生产者与消费者。其实,还有一件更“一揽子”的工具,能把互斥和等待都装进去——信号量

一个计数加一个队列

  信号量的核心,其实只有一个整数和一个等待队列:

  • 一个计数,表示“现在还剩多少份资源”
  • 一个等待队列,存着暂时拿不到资源的线程

  它只对外暴露两个动作,习惯上叫 PV

  • P(等待):把计数减一。如果减完小于 0,说明资源已经被占光,当前线程进入等待队列睡下。
  • V(发信号):把计数加一。如果还有线程在等,就唤醒其中一个。

  一减一加之间,信号量就把“抢资源、排队、通知”全包了。

当锁用:计数为 1

  如果把计数初始化成 1,信号量就退化成一个互斥锁:

  • P:计数从 1 减到 0,成功进入;再来一个就减到 -1,只能等
  • V:计数加回去,唤醒一个等待者

  所以,互斥锁可以看作信号量在“计数为 1”时的特例,这种用法叫二元信号量

管理多份资源:计数大于 1

  信号量真正比锁强的地方,是计数可以大于 1。

  比如系统里有 3 台打印机,就把信号量初始化成 3:每台被占用时 P 一下,用完 V 一下。第 4 个来申请的线程会被挡住,直到有人还回来。这样,一行计数就替我们管住了“有限资源”。

  用信号量重写生产者与消费者也很简洁:用两个信号量分别表示“空位数量”和“数据数量”,生产者 P 空位、V 数据,消费者反过来。计数器天然表达了缓冲区里还有多少空位、多少数据,省去了手写条件判断。

它与条件变量的关系

  信号量和条件变量,解决的都是“等待与唤醒”。区别在于:

  • 条件变量只负责等和叫,条件本身要自己用变量判断
  • 信号量自带计数,条件就是“还有没有资源”

  可以说,信号量是一种“已经打包好了条件”的同步原语,用起来更省事,但灵活性稍低。

思考题

  把信号量的计数初始化成 3,三个线程各自 P 一次后,第四个线程来 P 会怎样?它什么时候才能等到?

小结

知识点

  • 信号量 = 计数 + 等待队列
  • P 减一并可能等待,V 加一并可能唤醒
  • 计数为 1 时当互斥锁;计数大于 1 时管理有限资源
  • 生产者与消费者也可用信号量实现

参考资料

  1. Wikipedia(zh):信号量:semaphore
  2. Wikipedia(zh):互斥锁:二元信号量

思考题答案(仅供参考)

  第四个线程 P 之后,计数从 0 变成 -1,说明资源已经用完,它会进入信号量的等待队列睡下。直到前面三个线程中有任意一个执行 V,计数回升、等待者被唤醒,它才有机会拿到资源继续运行。这正是信号量“计数管理有限资源”的用法:超过可用份数的申请,自动排队。

协议

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

封面图

设计师 | 南国微雪