Semaphore(信号量)是什么?它通常被用于解决什么场景下的并发问题?
Semaphore(信号量)是计算机科学中用于控制多个进程或线程对共享资源进行访问的一种并发同步机制。它由荷兰计算机科学家艾兹赫尔·戴克斯特拉(Edsger W. Dijkstra)于1965年提出。
通俗地讲,信号量就像是一个停车场的入口显示牌。
- 停车场有10个车位(初始信号量为10)。
- 每进去一辆车,显示牌上的可用车位就减1(信号量减1)。
- 当可用车位为0时,道闸就不会打开,后续的车辆必须在门外排队等待(线程阻塞)。
- 当里面有一辆车开出来,可用车位加1(信号量加1),道闸抬起,排在最前面的一辆等待的车可以进入。
一、 信号量的核心机制
在底层实现上,信号量本质上是一个非负整数计数器,并配有两个原子操作(在操作系统教材中通常称为 P 操作和 V 操作):
- Acquire(获取 / P操作 / Wait): 线程尝试获取资源。
- 如果信号量的值 > 0,则将其减 1,线程继续执行。
- 如果信号量的值 == 0,则线程被阻塞(挂起),直到信号量的值大于0。
- Release(释放 / V操作 / Signal): 线程释放资源。
- 将信号量的值加 1。
- 如果此时有其他线程因为等待该信号量而被阻塞,唤醒其中一个(或多个)线程。
根据计数器的取值范围,信号量分为:
- 计数信号量 (Counting Semaphore): 初始值大于1,允许多个线程同时访问某种资源。
- 二值信号量 (Binary Semaphore): 初始值只能是0或1,功能上类似于互斥锁(Mutex),但机制不同。
二、 信号量通常用于解决什么并发场景?
信号量在并发编程中非常灵活,主要用于解决以下几类经典场景:
1. 资源限流 / 限制并发访问数(最常见用法)
当系统中有某种有限的公共资源时,可以使用计数信号量来限制同时访问该资源的线程数量,防止系统过载。
- 场景举例:
- 数据库连接池: 假设连接池中只有20个数据库连接,那么可以创建一个初始值为20的信号量。每个需要查数据库的线程必须先
acquire,用完后再release。第21个线程会自动排队等待。 - API 接口限流: 限制某个高开销接口最多只允许并发100个请求。
- 文件下载器: 限制同时最多只能有3个下载任务进行。
- 数据库连接池: 假设连接池中只有20个数据库连接,那么可以创建一个初始值为20的信号量。每个需要查数据库的线程必须先
2. 线程 / 进程间的执行顺序同步(协作)
信号量可以用来强制规定多个线程的执行顺序,即“线程A的某个操作必须在线程B的某个操作之后发生”。
- 场景举例: 假设有线程 A 和线程 B,要求 B 必须等 A 计算出初步结果后才能执行。
- 做法: 创建一个初始值为 0 的信号量。
- 线程 B 一开始就执行
acquire(),因为是0,所以 B 直接被阻塞挂起。 - 线程 A 执行完毕后,调用
release(),信号量变成1,此时系统会自动唤醒 B,B 接着往下执行。
- 线程 B 一开始就执行
3. 生产者-消费者问题 (Producer-Consumer Problem)
这是并发编程中最经典的协同模型,通常使用两个信号量(配合互斥锁)来解决:
- 场景举例: 一个固定大小的队列,生产者往里塞数据,消费者往外取数据。
- 做法:
- 信号量
empty:表示队列中剩余的空槽位数,初始值为队列容量 N。 - 信号量
full:表示队列中已有的商品数,初始值为 0。 - 生产者: 必须先
acquire(empty)(等有空位),然后生产,最后release(full)(告诉消费者有货了)。 - 消费者: 必须先
acquire(full)(等有货),然后消费,最后release(empty)(告诉生产者腾出空位了)。
- 信号量
4. 替代互斥锁(二值信号量)
如果把信号量的初始值设为 1,它就可以当成互斥锁(Mutex)来使用,用来保护临界区代码,确保同一时刻只有一个线程在修改共享数据。
- 注:虽然二值信号量可以做互斥,但现代编程中保护临界区一般首选互斥锁(Mutex)。因为 Mutex 拥有“持有者(Owner)”的概念,必须由加锁的那个线程去解锁;而 Semaphore 没有所有权概念,线程 A
acquire的信号量,线程 B 可以去release,这在同步协作时很方便,但在保护数据时容易引发混乱。
总结
- 它是什么: 一个带原子的加减操作和阻塞唤醒机制的计数器。
- 解决什么问题: 主要解决多线程环境下的资源数量控制(限流),以及多个线程之间的执行顺序协调(同步)。
右滑查看面试常问