Python 异步编程实战:asyncio.gather 与信号量的并发限流三板斧

举报
yd_257742204 发表于 2026/11/08 06:11:00 2026/11/08
【摘要】 gather 是并发发射器不是限流器,Semaphore 才是闸门,CPU 密集丢线程池——附 300 接口实测数据与三个坑

Python 异步编程实战:asyncio.gather 与信号量的并发限流三板斧

用 Python 写爬虫、批量调用接口时,新手最常犯的不是"不会异步",而是"一异步就把自己打挂":300 个请求同时发出,对方直接 429 甚至封 IP。这篇记录我在把一个同步批量脚本改造成 asyncio 时的三个关键台阶。

台阶一:gather 是并发发射器,不是限流器

asyncio.gather(*tasks) 会把所有协程同时提交事件循环。对 300 个 URL 写 await asyncio.gather(*[fetch(u) for u in urls]),等于 300 个连接齐发——本机端口、对端限流、内存三处都吃不消。gather 的价值在"收集全部结果",它的 return_exceptions=True 参数让单个失败不炸整批:

results = await asyncio.gather(*tasks, return_exceptions=True)
ok = [r for r in results if not isinstance(r, Exception)]

台阶二:Semaphore 才是限流闸门

正确姿势是 asyncio.Semaphore(N) 包住每个协程内部:

sem = asyncio.Semaphore(10)

async def fetch(url):
    async with sem:
        return await client.get(url)

三个坑:

  1. Semaphore 要在事件循环内创建(Py3.10 前跨循环复用会报 got Future attached to a different loop),统一放 main() 里生成再传参。
  2. 限的是"进入"不是"完成":async with sem 包住整个请求没问题,但如果在 acquire 之后又 spawn 子任务,实际并发会超 N。
  3. 超时要逐请求设:gather 没有整体 timeout 参数,asyncio.wait_for(fetch(u), 15) 或外层 asyncio.timeout()(3.11+)二选一。

台阶三:CPU 密集部分必须丢进线程池

异步只解决 IO 等待。把响应做 JSON 解析后再做重计算时,事件循环会被阻塞,信号量形同虚设。用 loop.run_in_executor(None, parse_fn, data) 或 asyncio.to_thread()(3.9+)把 CPU 段摘出去。

实测数字

同一批 300 个接口调用:同步串行 41.8s;裸 gather 并发 6.2s 但 27 个 429;Semaphore(10) + wait_for(15) 并发 8.9s、0 失败。并发数不是越大越好,对方限流阈值决定了"快"的上限。

结论

  • gather 负责组织,Semaphore 负责排队,to_thread 负责 CPU——三者各司其职。
  • return_exceptions=True 几乎总是该加,否则一个超时废掉整批结果。
  • 判断限流是否生效,看对端状态码分布比看本机耗时更准。

(原创实操笔记,代码均在本机 Python 3.11 环境跑过)

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。