
Python 信号量详解:并发控制实战
- [一、Python 信号量详解](#一、Python 信号量详解)
-
- 1、引言
- [2、 什么是信号量](#2、 什么是信号量)
-
- [2.1 、信号量的核心概念](#2.1 、信号量的核心概念)
- 3、信号量的基本用法
-
- [3.1、 创建信号量](#3.1、 创建信号量)
- [3.2、 获取与释放](#3.2、 获取与释放)
- [3.3、 完整示例:限制并发线程数](#3.3、 完整示例:限制并发线程数)
- 4、信号量的高级用法
-
- [4.1、 非阻塞获取](#4.1、 非阻塞获取)
- [4.2 、带超时的获取](#4.2 、带超时的获取)
- [4.3 、有界信号量](#4.3 、有界信号量)
- [4.4、 信号量与锁的区别](#4.4、 信号量与锁的区别)
- [5、 信号量的实际应用场景](#5、 信号量的实际应用场景)
-
- [5.1 、连接池管理](#5.1 、连接池管理)
- [5.2、 限流控制](#5.2、 限流控制)
- [5.3 、生产者-消费者模式](#5.3 、生产者-消费者模式)
- 6、多进程中的信号量
- [7、 注意事项与常见陷阱](#7、 注意事项与常见陷阱)
-
- [7.1 、死锁风险](#7.1 、死锁风险)
- [7.2 、忘记释放](#7.2 、忘记释放)
- [7.3 、信号量与 GIL](#7.3 、信号量与 GIL)
- [7.4、 性能开销](#7.4、 性能开销)
- [8、 总结](#8、 总结)
- 二、代码示例

一、Python 信号量详解
1、引言
在多线程与多进程编程中,控制并发访问共享资源是一项核心挑战。Python 提供了多种同步原语,其中信号量(Semaphore)是一种灵活且强大的工具,用于限制同时访问某个资源的线程或进程数量。本文将基于 Python 最新版本,深入讲解信号量的原理、用法、常见应用场景以及注意事项,帮助你写出更健壮的并发程序。
2、 什么是信号量
信号量是一种计数器,用于控制对共享资源的访问权限。它维护一个内部计数器,当线程或进程需要访问资源时,必须先获取信号量(计数器减一);当访问结束时,释放信号量(计数器加一)。当计数器为零时,后续的获取操作将被阻塞,直到有其他线程释放信号量。
信号量由计算机科学家 Edsger Dijkstra 于 1965 年提出,是操作系统中经典的同步机制。在 Python 中,信号量被封装在 threading 和 multiprocessing 两个标准库中,接口保持一致。
2.1 、信号量的核心概念
- 计数器(Counter):表示当前可用的资源数量。
- 获取(Acquire):计数器减一,若计数器为零则阻塞等待。
- 释放(Release):计数器加一,并唤醒一个等待的线程或进程。
3、信号量的基本用法
Python 中信号量的使用非常简单,核心 API 包括构造、获取和释放三个操作。
3.1、 创建信号量
python
import threading
# 创建一个初始值为 3 的信号量
semaphore = threading.Semaphore(3)
在 multiprocessing 模块中,用法完全一致:
python
from multiprocessing import Semaphore
semaphore = Semaphore(3)
3.2、 获取与释放
python
# 获取信号量(计数器减一)
semaphore.acquire()
# 释放信号量(计数器加一)
semaphore.release()
推荐使用上下文管理器(with 语句),它可以确保即使发生异常也能正确释放信号量:
python
with semaphore:
# 访问共享资源
pass
3.3、 完整示例:限制并发线程数
下面是一个完整的示例,演示如何使用信号量限制同时运行的线程数量:
python
import threading
import time
# 创建一个最多允许 2 个线程同时访问的信号量
semaphore = threading.Semaphore(2)
def worker(worker_id):
with semaphore:
print(f"Worker {worker_id} 开始工作")
time.sleep(2)
print(f"Worker {worker_id} 结束工作")
threads = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
threads.append(t)
t.start()
for t in threads:
t.join()
运行结果:
Worker 0 开始工作
Worker 1 开始工作
Worker 0 结束工作
Worker 2 开始工作
Worker 1 结束工作
Worker 3 开始工作
Worker 2 结束工作
Worker 4 开始工作
Worker 3 结束工作
Worker 4 结束工作
可以看到,任意时刻最多只有 2 个线程在同时执行。
4、信号量的高级用法
4.1、 非阻塞获取
acquire() 方法支持 blocking 参数,可以实现非阻塞获取:
python
# 非阻塞获取,若信号量不可用则立即返回 False
if semaphore.acquire(blocking=False):
try:
# 访问共享资源
pass
finally:
semaphore.release()
else:
print("信号量不可用,稍后再试")
4.2 、带超时的获取
从 Python 3.x 开始,acquire() 支持 timeout 参数,可以在指定时间内等待信号量:
python
# 最多等待 3 秒
if semaphore.acquire(timeout=3):
try:
# 访问共享资源
pass
finally:
semaphore.release()
else:
print("等待超时,未能获取信号量")
4.3 、有界信号量
threading.BoundedSemaphore 是信号量的一个变体,它确保释放次数不会超过获取次数,从而避免因编程错误导致计数器无限增长:
python
import threading
# 有界信号量
bounded_semaphore = threading.BoundedSemaphore(2)
bounded_semaphore.acquire()
bounded_semaphore.release()
bounded_semaphore.release() # 这里会抛出 ValueError
运行上述代码会抛出 ValueError: Semaphore released too many times,帮助开发者及时发现错误。
4.4、 信号量与锁的区别
信号量和锁(Lock)都是同步原语,但它们的语义不同:
| 特性 | 锁(Lock) | 信号量(Semaphore) |
|---|---|---|
| 计数器 | 只有 0 和 1 两种状态 | 可以是任意非负整数 |
| 用途 | 互斥访问 | 控制并发数量 |
| 可重入 | 否(RLock 可重入) |
否 |
| 典型场景 | 保护临界区 | 限流、连接池 |
简单来说,锁是信号量计数器为 1 的特例,而信号量是锁的泛化。
5、 信号量的实际应用场景
5.1 、连接池管理
在数据库或网络连接池中,信号量可以限制同时打开的连接数:
python
import threading
import time
class ConnectionPool:
def __init__(self, max_connections):
self.semaphore = threading.Semaphore(max_connections)
self.connections = []
def get_connection(self):
self.semaphore.acquire()
# 模拟获取连接
conn = f"Connection-{len(self.connections) + 1}"
self.connections.append(conn)
return conn
def release_connection(self, conn):
# 模拟释放连接
self.connections.remove(conn)
self.semaphore.release()
5.2、 限流控制
在 Web 爬虫或 API 调用中,信号量可以限制请求速率,避免对目标服务器造成过大压力:
python
import threading
import time
import requests
# 限制同时最多 3 个请求
semaphore = threading.Semaphore(3)
def fetch_url(url):
with semaphore:
response = requests.get(url)
print(f"Fetched {url}: {response.status_code}")
return response
urls = ["https://example.com"] * 10
threads = [threading.Thread(target=fetch_url, args=(url,)) for url in urls]
for t in threads:
t.start()
for t in threads:
t.join()
5.3 、生产者-消费者模式
信号量也可以用于实现生产者-消费者模式,控制缓冲区中元素的数量:
python
import threading
import time
import random
# 缓冲区容量为 5
empty = threading.Semaphore(5) # 空位数量
full = threading.Semaphore(0) # 已占用位置数量
buffer = []
def producer():
for i in range(10):
empty.acquire()
item = random.randint(1, 100)
buffer.append(item)
print(f"生产: {item}")
full.release()
time.sleep(0.5)
def consumer():
for _ in range(10):
full.acquire()
item = buffer.pop(0)
print(f"消费: {item}")
empty.release()
time.sleep(1)
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()
6、多进程中的信号量
multiprocessing 模块提供了与 threading 一致的信号量接口,用于多进程间的同步:
python
from multiprocessing import Process, Semaphore
import time
def worker(semaphore, worker_id):
with semaphore:
print(f"Process {worker_id} 开始工作")
time.sleep(2)
print(f"Process {worker_id} 结束工作")
if __name__ == "__main__":
semaphore = Semaphore(2)
processes = []
for i in range(5):
p = Process(target=worker, args=(semaphore, i))
processes.append(p)
p.start()
for p in processes:
p.join()
需要注意的是,多进程信号量在 Windows 和 Linux 上的行为略有差异,但基本用法保持一致。
7、 注意事项与常见陷阱
7.1 、死锁风险
信号量使用不当可能导致死锁。例如,线程在持有信号量时又去获取另一个信号量,可能造成循环等待。应尽量避免嵌套获取多个信号量。
7.2 、忘记释放
如果使用 acquire() / release() 手动管理,忘记释放会导致信号量计数器逐渐减少,最终所有线程都被阻塞。推荐始终使用 with 语句。
7.3 、信号量与 GIL
在 CPython 中,由于全局解释器锁(GIL)的存在,多线程无法真正并行执行 CPU 密集型任务。信号量主要用于 I/O 密集型任务的并发控制,对于 CPU 密集型任务,应使用多进程。
7.4、 性能开销
信号量的获取和释放涉及系统调用,频繁操作会带来一定的性能开销。在高频场景下,应尽量减少信号量的使用频率。
8、 总结
信号量是 Python 并发编程中一个强大而灵活的同步工具。通过本文的学习,我们掌握了:
- 信号量的基本概念和工作原理;
- 使用
threading.Semaphore和multiprocessing.Semaphore控制并发数量; - 非阻塞获取、超时获取和有界信号量等高级用法;
- 连接池、限流和生产者-消费者等实际应用场景;
- 使用信号量时的注意事项和常见陷阱。
在实际开发中,合理使用信号量可以有效控制资源访问,提升程序的稳定性和性能。建议在编写并发代码时,优先使用 with 语句管理信号量,并时刻警惕死锁和资源泄漏的风险。
二、代码示例
python
import threading
import time
# ====================== 1. 普通 Semaphore 基础演示 ======================
def demo_semaphore_basic():
print("=" * 60)
print("【1.普通 Semaphore(2):最多允许2个线程同时执行】")
sem = threading.Semaphore(value=2)
def worker(idx):
# with 自动 acquire + 退出自动 release
with sem:
print(f"线程 {idx} 拿到信号量,进入临界区")
time.sleep(1.0) # 模拟耗时任务
print(f"线程 {idx} 释放信号量,退出临界区")
thread_list = []
for i in range(5):
t = threading.Thread(target=worker, args=(i,))
thread_list.append(t)
t.start()
for t in thread_list:
t.join()
print("Semaphore 基础演示结束\n")
# ====================== 2. acquire 非阻塞 & timeout 超时特性 ======================
def demo_semaphore_timeout():
print("=" * 60)
print("【2.acquire(block=False) 非阻塞 / timeout 超时等待】")
sem = threading.Semaphore(1)
sem.acquire() # 先占用信号量
def try_get(id):
# 非阻塞获取
ok1 = sem.acquire(blocking=False)
print(f"线程{id} 非阻塞获取结果: {ok1}")
# 最多等待0.5秒
ok2 = sem.acquire(timeout=0.5)
print(f"线程{id} 等待0.5s获取结果: {ok2}")
t = threading.Thread(target=try_get, args=(1,))
t.start()
t.join()
sem.release()
print("超时演示结束\n")
# ====================== 3. BoundedSemaphore 有界信号量(防止超额release) ======================
def demo_bounded_semaphore():
print("=" * 60)
print("【3.BoundedSemaphore:禁止无限制release】")
bsem = threading.BoundedSemaphore(value=2)
bsem.acquire()
bsem.release()
print("正常release一次成功")
try:
# 初始值=2,没有acquire就直接release,计数器溢出
bsem.release()
bsem.release()
bsem.release()
except ValueError as e:
print(f"BoundedSemaphore 非法释放捕获异常: {e}")
print("BoundedSemaphore演示结束\n")
# ====================== 4. 手动 acquire / try‑finally 安全释放(不使用with) ======================
def demo_manual_acquire():
print("=" * 60)
print("【4.手动 acquire / try‑finally 保证释放】")
sem = threading.Semaphore(1)
def task():
acquired = False
try:
acquired = sem.acquire(timeout=1)
if acquired:
print("手动拿到信号量,执行业务逻辑")
time.sleep(0.3)
else:
print("获取信号量超时")
finally:
if acquired:
sem.release()
print("finally中释放信号量")
t = threading.Thread(target=task)
t.start()
t.join()
print("手动管理信号量演示结束\n")
# ====================== 5. 信号量实现生产者消费者(和Queue对比) ======================
def demo_producer_consumer_sem():
print("=" * 60)
print("【5.信号量实现生产者-消费者模型】")
empty = threading.Semaphore(3) # 空位数量
full = threading.Semaphore(0) # 产品数量
buffer = []
lock = threading.Lock()
def producer(num):
for i in range(2):
empty.acquire()
with lock:
item = f"data-{num}-{i}"
buffer.append(item)
print(f"生产者{num}生产: {item}, buffer={buffer}")
full.release()
time.sleep(0.2)
def consumer(num):
for _ in range(2):
full.acquire()
with lock:
item = buffer.pop(0)
print(f"消费者{num}消费: {item}, buffer={buffer}")
empty.release()
time.sleep(0.4)
t1 = threading.Thread(target=producer, args=(1,))
t2 = threading.Thread(target=consumer, args=(1,))
t1.start()
t2.start()
t1.join()
t2.join()
print("生产者消费者信号量演示结束\n")
if __name__ == "__main__":
demo_semaphore_basic()
demo_semaphore_timeout()
demo_bounded_semaphore()
demo_manual_acquire()
demo_producer_consumer_sem()
python
D:\user\01417804\桌面\PythonProject\.venv\Scripts\python.exe D:\user\01417804\桌面\PythonProject\main.py
============================================================
【1.普通 Semaphore(2):最多允许2个线程同时执行】
线程 0 拿到信号量,进入临界区
线程 1 拿到信号量,进入临界区
线程 0 释放信号量,退出临界区
线程 1 释放信号量,退出临界区线程 2 拿到信号量,进入临界区
线程 3 拿到信号量,进入临界区
线程 3 释放信号量,退出临界区线程 2 释放信号量,退出临界区
线程 4 拿到信号量,进入临界区
线程 4 释放信号量,退出临界区
Semaphore 基础演示结束
============================================================
【2.acquire(block=False) 非阻塞 / timeout 超时等待】
线程1 非阻塞获取结果: False
线程1 等待0.5s获取结果: False
超时演示结束
============================================================
【3.BoundedSemaphore:禁止无限制release】
正常release一次成功
BoundedSemaphore 非法释放捕获异常: Semaphore released too many times
BoundedSemaphore演示结束
============================================================
【4.手动 acquire / try‑finally 保证释放】
手动拿到信号量,执行业务逻辑
finally中释放信号量
手动管理信号量演示结束
============================================================
【5.信号量实现生产者-消费者模型】
生产者1生产: data-1-0, buffer=['data-1-0']
消费者1消费: data-1-0, buffer=[]
生产者1生产: data-1-1, buffer=['data-1-1']
消费者1消费: data-1-1, buffer=[]
生产者消费者信号量演示结束
进程已结束,退出代码为 0

