asyncio.Semaphore 是协程级并发控制器,能限流但非请求级限流器;它通过共享计数器限制同时执行的协程数,需全局复用实例并配合 async with 确保安全释放。

asyncio.Semaphore 是什么,它真能限流吗?
能,但它不是“请求限流器”,而是协程级别的**并发数控制器**。它不感知 HTTP、不统计 QPS、不自动重试,只管“此刻最多几个 await 块在同时跑”。如果你要压住对下游 API 的并发请求数(比如避免触发 429),asyncio.Semaphore 是最轻量且直接有效的手段。
常见错误是把它当“每秒最多 N 次”的令牌桶用——它做不到。它只做:同一时间,最多 N 个协程能 acquire() 成功并继续执行;其余协程会在 acquire() 处挂起,等别人 release() 后才唤醒。
怎么正确初始化和共享一个 Semaphore?
必须在事件循环启动前创建,并被所有需要限流的协程共用同一个实例。常见坑是每次调用都新建一个 asyncio.Semaphore(5),那等于没限——每个协程都拿到自己的“空信号量”,互不影响。
推荐做法:
- 作为模块级变量定义:
sem = asyncio.Semaphore(3) - 或作为类属性(如
self.sem = asyncio.Semaphore(3)),确保实例复用 - 不要在
async def函数里初始化,也不传给asyncio.create_task()时临时构造
示例片段:
import asyncio
<p>sem = asyncio.Semaphore(2) # 全局共享,最多 2 个并发</p><p>async def fetch(url):
async with sem: # 等价于 await sem.acquire() + try/finally release()
print(f"start {url}")
await asyncio.sleep(1) # 模拟 IO
print(f"done {url}")</p><p>async def main():
await asyncio.gather(
fetch("a.com"),
fetch("b.com"),
fetch("c.com"),
fetch("d.com"),
)
</p>
async with sem 和手动 acquire/release 有什么区别?
本质一样,但 async with sem 更安全——它保证即使协程抛异常、提前 return 或被 cancel,release() 也会执行。手动写容易漏掉 release(),导致信号量永久卡死(后续所有协程永远阻塞在 acquire())。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
以下写法危险:
await sem.acquire()
try:
await do_something()
except Exception:
# 忘了 release!
raise
应该用:
-
async with sem:—— 推荐,简洁可靠 - 或显式
try/finally包裹release(),但没必要多写三行
为什么并发数设成 5,实际还是看到 6 个请求发出去了?
常见原因有三个:
- 你用了多个
Semaphore实例(比如不同模块各建一个) - 部分请求绕过了
async with sem:(例如同步 HTTP 库requests.get()被混在async def里调用) - 下游服务本身做了连接池复用(如
aiohttp.TCPConnector(limit=10)),而你只限制了协程数,没限制底层连接数——这时需同步配置连接池limit和limit_per_host
验证是否生效:在 async with sem: 块内加 print(f"enter {id(asyncio.current_task())}"),观察同一时刻最多几条输出紧挨着出现。
真正难控的是跨进程、跨服务的全局流量。Semaphore 只管当前 Python 进程内的协程调度——这点常被忽略。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










