用aiohttp替代requests、asyncio.gather()并发、Semaphore限流,100个HTTP请求耗时可从50秒降至约1秒;requests同步阻塞导致串行等待,而协程在await处挂起并由事件循环调度实现真正并发。

直接上结论:用 aiohttp 替掉 requests,用 asyncio.gather() 并发发请求,再加个 asyncio.Semaphore 控制并发数——这三步做完,100 个 HTTP 请求的耗时通常能从 50 秒压到 1 秒左右,前提是目标服务本身不拖后腿。
为什么 requests 一并发就慢得离谱
同步阻塞是根源。requests.get() 调用后,线程立刻卡住,CPU 空转等响应。100 个请求串行,就是 100 × 单次延迟;哪怕每个只要 300ms,总耗时也逼近 30 秒。
这不是网络或服务器的问题,是你代码没让出控制权。协程在 await 处挂起,事件循环立刻切到下一个任务——这才是并发的本质。
- 别试图给
requests加async包装器,它底层是同步 socket,强行 await 只会让整个事件循环卡死 -
threading或multiprocessing能绕过,但开销大、难调试、资源占用高,属于“用锤子砸螺丝” - 真正该换的是 I/O 库:HTTP 用
aiohttp,数据库用asyncpg或aiomysql,文件读写用asyncio.to_thread()(Python 3.9+)或loop.run_in_executor()
aiohttp + asyncio.gather() 的最小可靠写法
不要一上来就写类、封装 session、搞 retry 逻辑。先跑通最简路径,确认异步生效:
import asyncio import aiohttp <p>async def fetch(url): async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.text()</p><p>async def main(): urls = ["<a href="https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c">https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c</a>"] * 10</p><h1>并发启动全部协程,不是逐个 await</h1><pre class="brush:php;toolbar:false;">results = await asyncio.gather(*[fetch(u) for u in urls]) print(len(results))
asyncio.run(main())
关键点:
-
asyncio.gather()是并发执行的开关,不是语法糖——它把所有协程注册进事件循环,由调度器统一管理 -
ClientSession()必须复用,不能每个请求都新建一个,否则连接池失效,还可能触发端口耗尽 - 如果某次请求失败(如超时、4xx/5xx),
gather()默认会抛异常中断全部;加return_exceptions=True可让它返回异常对象而非崩溃
不加信号量(Semaphore)的并发=自爆现场
无限制并发看似爽,实际会迅速打满本地端口、耗尽远端连接池、触发对方限流甚至封 IP。真实场景必须限流。
正确姿势是用 asyncio.Semaphore 包裹 I/O 操作:
sem = asyncio.Semaphore(20) <p>async def fetch_limited(url): async with sem: # 这里会阻塞直到有许可 async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.text() </p>
常见误区:
- 把
Semaphore放在ClientSession()外层(比如只包 session 创建)——没用,真正吃资源的是session.get() - 设成 1000 以为“越大越好”,结果本地报
OSError: [Errno 24] Too many open files - 忽略 DNS 解析和 TCP 建连耗时,以为并发数 = QPS,其实受
aiohttp.TCPConnector(limit_per_host)和系统 ulimit 共同制约
最后提醒一句:asyncio 不是银弹。如果你的瓶颈在 CPU 计算(比如解析巨量 JSON、图像处理),async 代码反而更慢——那种情况该交给 loop.run_in_executor() 或直接切到 multiprocessing。真正的优化,永远从识别瓶颈开始,而不是盲目套 async。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











