数码知识屋
霓虹主题四 · 更硬核的阅读氛围

深入理解线程同步机制中的信号量

发布时间:2025-12-20 16:10:49 阅读:168 次

信号量在多线程环境中的作用

服务器在处理大量并发请求时,经常需要多个线程协作完成任务。比如一个电商平台的库存扣减操作,如果多个线程同时读取同一商品的库存值,都判断还有货,接着各自执行减一操作,最后可能导致库存变成负数。这类问题就是典型的资源竞争,而信号量正是用来控制这种并发访问的核心机制之一。

信号量本质上是一个计数器,用来表示可用资源的数量。线程在访问共享资源前必须先获取信号量,如果当前计数大于零,就允许通行并减少计数;如果为零,线程就会被阻塞,直到有其他线程释放资源。

二进制信号量与计数信号量

最常见的信号量有两种:二进制信号量和计数信号量。二进制信号量只有0和1两个状态,相当于一把钥匙,只能一个人用。它常用于实现互斥锁(mutex),确保同一时间只有一个线程能进入临界区。

而计数信号量可以设置更大的初始值,比如5,意味着最多允许5个线程同时访问某个资源池。这在数据库连接池管理中很常见——系统限制最多同时打开5个连接,超过的请求就得排队等。

实际代码中的信号量使用

以 Python 的 threading 模块为例,可以通过 Semaphore 类来创建信号量:

import threading
import time

# 设置最多3个线程可同时运行
sem = threading.Semaphore(3)

def worker(worker_id):
print(f"工人 {worker_id} 尝试获取许可...")
with sem:
print(f"工人 {worker_id} 开始工作")
time.sleep(2) # 模拟工作耗时
print(f"工人 {worker_id} 完成工作")

# 启动10个工人
for i in range(10):
t = threading.Thread(target=worker, args=(i,))
t.start()

上面这段代码中,虽然启动了10个线程,但因为信号量限制为3,所以每次最多只有3个线程能真正开始工作,其余的会自动等待前面的释放资源后再依次进入。

避免死锁的关键点

使用信号量时最容易踩的坑是忘记释放。比如某个线程拿到了信号量,但在执行过程中抛出异常直接退出,没有正确释放,那么后面的线程就会一直卡住,整个服务可能因此停滞。解决办法是在获取信号量的操作外层加上异常保护,或者像上面例子中使用上下文管理器(with 语句),让语言本身保证释放动作一定会执行。

另外,在复杂的调用链中,要避免多个信号量之间的循环等待。比如线程A持有信号量1并等待信号量2,而线程B持有信号量2又反过来等信号量1,这就形成了死锁。合理的做法是统一规定信号量的申请顺序,大家都按同一个顺序来,就能打破这种僵局。

信号量与其他同步机制的对比

除了信号量,常见的还有互斥锁、条件变量、读写锁等。互斥锁更适合单一资源的排他访问,而信号量更灵活,能控制资源池的容量。条件变量则通常配合互斥锁使用,用于线程间的“通知-等待”场景,比如生产者消费者模型。

在实际的服务器维护中,选择哪种机制取决于具体需求。如果只是防止两个线程同时改同一个配置文件,用互斥锁就够了;但如果要控制并发上传的请求数量,防止磁盘IO过载,信号量就是更合适的选择。