sem = asyncio.Semaphore(MAX_CONCURRENT) async with sem: 语法详解
这两行代码是 Python
asyncio 中非常经典的并发控制组合。简单来说,Semaphore(信号量)就像一个“限流器”,而 async with 则是确保这个限流器能安全、自动管理的语法糖。下面为你详细拆解:
1. sem = asyncio.Semaphore(MAX_CONCURRENT)
这行代码创建了一个信号量对象,并设置了最大并发数
MAX_CONCURRENT。- 内部机制:你可以把它想象成一个只有
MAX_CONCURRENT个座位的候车室,或者一个计数器。 - 初始状态:创建时,计数器等于
MAX_CONCURRENT。 - 核心动作:
- 当有协程想要执行受保护的代码时,需要调用
await sem.acquire()。如果计数器 > 0,计数器减 1,协程继续执行;如果计数器 == 0,协程就会挂起(等待),直到有其他协程释放信号量。 - 当协程执行完毕,调用
sem.release(),计数器加 1,并唤醒一个正在等待的协程。
- 当有协程想要执行受保护的代码时,需要调用
2. async with sem:
这是 Python 的异步上下文管理器语法。它完美地封装了上面的“获取”和“释放”动作。
当你写下
async with sem: 时,Python 在底层会自动帮你做以下事情:# async with sem: 的底层等价逻辑:
await sem.acquire() # 1. 进入代码块前:尝试获取信号量(如果满了就在此挂起等待)
try:
# 2. 执行 async with 下面的代码块
await do_something()
finally:
sem.release() # 3. 无论代码块是否报错,退出时都会自动释放信号量
为什么强烈建议用
因为
async with 而不是手动 acquire/release?因为
async with 能防止死锁。如果你的业务代码抛出异常,手动写的 release() 可能会被跳过,导致信号量永远无法归还,其他协程会永久卡死。而 async with 的 finally 机制保证了信号量一定会被释放。3. 完整实战示例
假设你要爬取 100 个网页,但服务器规定最多只能同时发 5 个请求:
import asyncio
MAX_CONCURRENT = 5
async def fetch(url, sem):
# 只有拿到信号量的协程才能进入这里
async with sem:
print(f"正在请求: {url}")
await asyncio.sleep(1) # 模拟网络请求耗时
print(f"请求完成: {url}")
async def main():
sem = asyncio.Semaphore(MAX_CONCURRENT)
urls = [f"url_{i}" for i in range(10)]
# 虽然创建了 10 个任务,但同一时刻最多只有 5 个在执行 fetch 内部逻辑
tasks = [fetch(url, sem) for url in urls]
await asyncio.gather(*tasks)
asyncio.run(main())
⚠️ 两个极易踩坑的注意事项
-
不要跨事件循环创建:
asyncio.Semaphore()必须在事件循环运行前或运行中创建,且绑定到当前循环。如果在多线程环境下,每个线程需要自己的信号量。 -
不要在信号量外做耗时操作:
# ❌ 错误示范:耗时操作在 async with 外面 async with sem: pass # 瞬间释放了信号量 await heavy_computation() # 这个耗时操作不受并发限制了! # ✅ 正确示范:耗时操作必须在 async with 内部 async with sem: await heavy_computation()
总结:
asyncio.Semaphore 负责限制并发数量,async with 负责安全地获取和释放。这两者结合是 Python 异步编程中实现“并发限流”的标准范式。
浙公网安备 33010602011771号