asyncio.Semaphore 是首选方案,因其协程安全、无额外依赖、精准控制并发数且不浪费事件循环资源;必须用 async with semaphore: 确保自动释放,避免永久挂起。

asyncio.Semaphore 为什么是首选方案
直接用 asyncio.Semaphore 控制并发数最稳妥。它不依赖时间窗口,只管“同时最多几个协程在跑”,适合防止下游服务被压垮或触发限流错误(比如返回 429 Too Many Requests)。比自己手写计时器或 sleep 更可靠,也比第三方库轻量——标准库自带,无额外依赖。
-
asyncio.Semaphore是协程安全的,多个await semaphore.acquire()不会冲突 - 它阻塞的是协程,不是线程,不会浪费事件循环资源
- 错误用法:在每次请求前
acquire()但忘记release(),会导致后续所有请求永久挂起 - 正确姿势:必须用
async with semaphore:确保自动释放
import asyncio import aiohttp <p>semaphore = asyncio.Semaphore(5) # 最多 5 个并发</p><p>async def fetch(url): async with semaphore: # 自动 acquire/release async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.text()</p>
如何把 QPS 限制转换成 Semaphore 并发数
QPS(每秒请求数)不能直接等同于 Semaphore 的值,得结合平均响应时间估算。比如目标 10 QPS,接口平均耗时 200ms,那理论上最多允许 10 × 0.2 = 2 个并发;如果耗时降到 50ms,就能撑到 5 并发。
- 实际中建议先设保守值(如 2~3),再根据监控调整
- 如果下游明确要求“每秒不超过 N 次”,且响应时间波动大,单靠
Semaphore不够,得加时间滑动窗口(比如用time.time()+ 队列记录最近请求时间戳) - 不要盲目设高并发数:即使网络快,aiohttp 连接池、DNS 缓存、TLS 握手都可能成为瓶颈
和 aiohttp.TCPConnector 的 max_connections 冲突吗
会,而且容易踩坑。aiohttp.TCPConnector(max_connections=10) 控制的是整个 session 的连接总数,而 asyncio.Semaphore(5) 控制的是并发发起的请求数。如果两者不协调,可能出现:
立即学习“Python免费学习笔记(深入)”;
Semaphore=10但max_connections=3→ 实际并发被 connector 截断,部分请求卡在连接等待
提示词大师-python版下载图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
Semaphore=2但max_connections=100→ 没问题,但浪费连接池资源建议让
semaphore≤max_connections,通常设为相同值更省心如果复用全局
ClientSession,务必确保connector在 session 初始化时就传入,而不是每次新建
connector = aiohttp.TCPConnector(limit=5) # 和 semaphore 值一致 session = aiohttp.ClientSession(connector=connector)
为什么不用 asyncio.sleep() 做匀速限流
asyncio.sleep() 可以凑出“每秒 N 次”的节奏,但只适合低频、确定性场景。真实 HTTP 请求耗时不固定,sleep 会叠加在网络延迟上,导致实际 QPS 远低于预期,甚至出现脉冲式请求。
- 例如:设
sleep(0.1)强制 10 QPS,但某次请求耗时 800ms,下一次立刻发出,瞬间就突破限制 - 更糟的是,sleep 不解决连接竞争问题,多个协程仍可能同时抢 DNS 解析或 TLS 握手
- 真需要匀速(比如对接支付网关),优先用
asyncio.Semaphore+ 请求时间戳队列做滑动窗口,而不是 sleep
真正难处理的是混合场景:既要控并发,又要控 QPS,还要兼容失败重试。这时候 semaphore 只是第一道闸门,后面还得补漏——比如重试时重新走限流逻辑,而不是无脑 retry。

















