asyncio.semaphore() 必须在协程外创建唯一实例并用 async with 确保自动释放,否则限流失效;它控制并发数而非速率,需结合下游承载力合理设值(通常5~20),并与connector.limit等机制区分使用。

asyncio.Semaphore() 怎么用才真正限制并发数
它不是“加了就生效”,得确保所有要限流的协程都 显式 await 同一个 semaphore 实例,否则完全没作用。
常见错误是:在循环里每次新建 asyncio.Semaphore(3),这等于每个协程拿自己的“3个名额”,实际并发还是全开。
- 必须在协程外(比如函数作用域顶部或类初始化时)创建唯一实例:
sem = asyncio.Semaphore(5) - 每个要限流的协程开头写:
await sem.acquire(),结尾配对sem.release()(推荐用async with sem:自动管理) - 别在
async with块里抛异常却不处理——没释放会导致信号量永久卡死
为什么用 async with 而不是手动 acquire/release
手动调用容易漏掉 release(),尤其遇到异常、return 提前退出、或者忘了写。一旦漏掉,semaphore 的计数就永远少一,后续协程全卡住等不到许可。
async with 是唯一可靠方式,它保证无论是否异常、是否提前 return,都会自动 release。
- 正确写法:
async with sem: await fetch_url(url) - 错误写法:
await sem.acquire() try: await fetch_url(url) finally: # 这里可能被跳过(比如没写 finally,或 finally 里又出错) sem.release()
和 aiohttp.ClientSession() 配合时的坑
很多人以为给 aiohttp.ClientSession() 加 connector=TCPConnector(limit=10) 就能限并发,其实这只是限制连接池大小,不等于限制并发协程数——100 个协程同时 await 一个 10 连接池的 session,照样会排队阻塞,但信号量早该拦在更上层。
- 信号量应放在业务逻辑入口(比如请求封装函数),而不是丢进
ClientSession配置里 - 如果同时用信号量 + connector.limit,注意两者目标不同:信号量控“发起请求数”,connector 控“同时建立的 TCP 连接数”
- HTTP/2 或复用连接场景下,connector.limit 可能比预期宽松;信号量才是你真正能精确控制的开关
限制并发数 ≠ 降低总耗时,小心反效果
设太小(比如 Semaphore(1))会让任务串行化,总时间接近求和;设太大(比如 Semaphore(100))又失去限流意义,还可能压垮下游服务或触发风控。
- 合理值取决于目标服务的承受力,通常从
5~20开始试,观察响应延迟和错误率 - 注意 DNS 解析、SSL 握手、网络抖动本身就会引入不可控延迟,信号量只管“允许几个协程进入临界区”,不管它们实际跑多慢
- 如果下游有明确 QPS 限制(比如 100req/s),单纯靠固定数量信号量不够,得结合
asyncio.sleep()或令牌桶做速率整形
信号量只是并发数的“门禁”,不是“节拍器”。门开了,里面的人跑多快、会不会撞墙,它不管。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











