什么是信号量?
想象你开车去一个停车场。入口有个计数器,显示还剩多少个空位。每进一辆车,计数减一;每出一辆车,计数加一。当计数为0时,入口栏杆就放下,后面的车只能排队等。
信号量(Semaphore)就是这套停车场的计数系统。
在编程中,当多个线程同时访问共享资源时,我们需要控制"同时访问的数量"。信号量就是一个计数器,用来限制并发访问某个资源的线程数量。
它是操作系统和编程语言中常用的同步原语,由计算机科学家 Dijkstra 在1965年提出。
为什么要懂信号量?
不懂信号量,你会遇到这些问题:
- 数据库连接池被同时请求打爆,服务器直接挂掉
- 文件被多个线程同时写入,内容乱成一团
- API调用频率失控,被对方服务器封IP
- 图片处理服务内存溢出,整个应用崩溃
信号量让你精确控制"同时能进多少人",避免资源被耗尽。
它特别适合处理这类场景:资源有限,但请求很多。比如你的服务器只有10个数据库连接,但来了50个并发请求。不用信号量,前10个请求成功,后40个直接报错。用了信号量,前10个先处理,处理完一个释放一个名额,后面的慢慢进来。
信号量的工作原理
信号量有一个核心操作:P操作(等待)和V操作(释放)。
- P操作:尝试获取一个"名额"。如果计数器大于0,就减一,放行;如果计数器等于0,就等待。
- V操作:释放一个"名额"。计数器加一,唤醒一个等待的线程。
最简单的情况是计数信号量,计数器可以是任意正整数。比如设置计数器为5,就意味着最多同时有5个线程访问资源。
还有一种特殊情况叫二值信号量,计数器只能是0或1。这相当于一个互斥锁,只允许一个线程访问。
举个栗子
假设有10个用户要访问数据库,但数据库最多同时处理3个连接。
import threading
# 信号量,最多允许3个线程同时访问
semaphore = threading.Semaphore(3)
def access_database(user_id):
semaphore.acquire() # 获取名额,P操作
try:
print(f"用户{user_id}正在访问数据库")
# 模拟数据库操作
import time
time.sleep(1)
print(f"用户{user_id}访问完成")
finally:
semaphore.release() # 释放名额,V操作
# 启动10个线程
threads = []
for i in range(10):
t = threading.Thread(target=access_database, args=(i+1,))
threads.append(t)
t.start()
for t in threads:
t.join()
运行结果:任意时刻最多只有3个线程在访问数据库。其他线程会排队等待,直到有名额释放。
实际开发中的使用场景
数据库连接池:限制同时连接数,防止数据库过载。这是最常见的用法,几乎每个后端项目都会遇到。
API速率限制:控制并发请求数,避免被目标服务器封IP。比如你调用第三方API,对方限制每秒100次请求,你就可以用信号量来控制。
资源管理器:比如图片处理服务,同时只能处理5张图片。超过5张就排队,避免内存溢出。
生产消费者模型:限制缓冲区大小,防止生产者塞太快,消费者处理不过来。
限流组件:像 Java 的 Guava RateLimiter、Python 的 aiolimiter 都是基于信号量实现的。
信号量和互斥锁的区别
很多人会混淆信号量和互斥锁。它们有什么区别?
- 互斥锁:只允许一个线程访问资源,计数器只能是0或1。
- 信号量:允许 N 个线程同时访问,计数器可以是任意正整数。
可以把互斥锁看作信号量的特例。信号量更灵活,适合控制并发数量;互斥锁更简单,适合保护临界区。
总结
信号量就是一个"名额计数器"。设置一个上限,谁先拿到名额谁就进场,没拿到的排队等。
记住:信号量控制的是并发数量,不是执行顺序。 它不关心哪个线程先执行,只关心同时能进多少个。
下次设计并发系统时,先问问自己:这个资源最多允许几个线程同时访问?答案就是信号量的初始值。