asyncio.Semaphore(int) 可以设置并发数,但仅限制显式acquire/release的临界区进入数量,不自动管理异常路径释放,须用async with确保安全;错误使用会导致协程永久阻塞。

asyncio.Semaphore 为什么不能直接传 int 当作并发数?
它确实可以,但很多人误以为 asyncio.Semaphore(5) 就能无条件限制“最多 5 个协程同时执行”——其实它只管你显式 acquire() / release() 的地方。如果漏掉 release()、或在异常路径里没处理,信号量会永久卡住,后续协程永远阻塞。
常见错误现象:asyncio.TimeoutError 或协程集体 hang 住,日志里看不到报错,CPU 占用低,但任务不推进。
- 必须用
async with semaphore:语法(自动确保释放),避免手写try/finally - 不要在同一个协程里多次
acquire()同一个 semaphore,除非你明确需要“重入”,而asyncio.Semaphore不支持重入 - 初始化值就是最大并发数,设为
0会导致所有协程立即阻塞,设为负数会抛ValueError
如何把 semaphore 正确注入到并发任务中?
关键不是“在哪里创建”,而是“谁持有、谁负责申请”。最安全的方式是把 semaphore 作为参数传进每个需要限流的协程内部,在其作用域内申请。
错误做法:在全局或类属性上存一个 semaphore,然后多个协程直接调用 await semaphore.acquire() —— 看似可行,但一旦某个协程崩溃未释放,整个池就废了。
立即学习“Python免费学习笔记(深入)”;
- 推荐结构:在主调度协程中创建
semaphore = asyncio.Semaphore(3),再用asyncio.gather(*[worker(semaphore, item) for item in items]) -
worker函数签名应为async def worker(sem: asyncio.Semaphore, data: Any),开头即async with sem: - 不要跨协程复用同一个
async with块;每个协程必须独立持有一段临界区
和 aiohttp.ClientSession 配合时的典型陷阱
很多人想用 semaphore 控制 HTTP 并发请求数,却把 semaphore 放在 ClientSession 外层,结果发现并发数还是超标——因为 aiohttp 内部有连接池,默认允许最多 100 个空闲连接,它不感知你的 semaphore。
真正起作用的是你在发起请求前是否被 semaphore 拦住,而不是响应返回后才释放。
- 正确顺序:
async with semaphore:→async with session.get(url):→ 处理响应 - 错误顺序:先
async with session.get(...),再async with semaphore:—— 请求早已发出,限流失效 - 如果还用了
session.connector.limit,注意它控制的是连接数,和 semaphore 控制的“并发协程数”是两层逻辑,建议只用 semaphore 统一管控
为什么有时候并发数看起来“不准”?
比如设了 Semaphore(4),但监控发现某时刻有 5 个请求在跑——这不是 bug,而是因为 asyncio.Semaphore 只保证“最多 N 个协程同时进入 async with 块”,不保证它们执行时间一致。一个协程刚进入临界区就 await 了 IO(比如读文件、等数据库响应),此时 CPU 让出,另一个协程可能紧接着也 acquire 成功。
换句话说:它控的是“并发进入临界区的数量”,不是“同时占用 CPU 的数量”,更不是“同一毫秒活跃连接数”。
- 如果你需要硬性绑定“任意时刻最多 N 个网络请求发出”,就得配合
aiohttp.TCPConnector(limit=4)使用,且确保所有请求都走同一个ClientSession - 如果任务本身包含 CPU 密集型操作(如解析大 JSON),
asyncio.Semaphore无法限制其 CPU 占用,得改用loop.run_in_executor+ 进程/线程池限流 - 调试时可用
len(semaphore._waiters)(非公开属性,仅调试)看当前排队等待的协程数,但别在生产环境依赖它
async with 的可靠性——只要没手动破坏,它基本不会出问题;但一旦混用 acquire() 和裸 await,或者在 __aexit__ 里抛新异常,信号量状态就会错乱。


















