asyncio.run()每次新建并关闭事件循环,反复调用会导致高开销和卡顿;正确做法是全局只启动一次,配合复用aiohttp.ClientSession、使用asyncio.gather并发请求、避免阻塞调用。

asyncio.run(main()) 启动方式不对,事件循环被反复创建
很多脚本跑起来就卡住或比同步还慢,根本原因是每次调用 asyncio.run() 都会新建一个事件循环——它开销不小,尤其在循环里反复调用时。比如把 asyncio.run(fetch_one_url()) 放在 for 循环里,等于每请求一次都重建 loop、初始化调度器、销毁协程对象。
- 正确做法:整个任务只启动一次 event loop,所有请求都在同一个
asyncio.run(main())里完成 - 别在协程内部再调
asyncio.run(),那会报RuntimeError: asyncio.run() cannot be called from a running event loop - 如果要从同步函数跳进异步流程,用
asyncio.create_task()或asyncio.ensure_future(),而不是重开 loop
aiohttp.ClientSession 没复用,TCP 连接反复握手
每个 fetch() 里写 async with aiohttp.ClientSession() as session: 看似干净,实则灾难:DNS 查询、TLS 握手、TCP 三次握手全得重来一遍。100 个请求可能产生 100 次 DNS 解析和 SSL 建连,耗时远超数据传输本身。
-
ClientSession必须作为参数传入协程,不能在每个请求里新建 - 连接池要显式配置:
TCPConnector(limit=30, ttl_dns_cache=300)——limit控制并发连接数,ttl_dns_cache缓存 DNS 结果,避免重复解析 - 别依赖默认 timeout,
ClientTimeout(total=10)要设,否则某个挂掉的请求会拖垮整批任务
await 写错位置,协程没真正并发
常见错误是把 await 放在循环里:for url in urls: await session.get(url)。这本质还是串行——等完第一个才发第二个,跟 requests 没区别。
- 并发必须靠
asyncio.gather(*tasks)或asyncio.create_task()批量提交 -
tasks = [fetch(session, url) for url in urls]是生成协程对象,不是执行;await asyncio.gather(*tasks)才真正并发触发 - 别混用
time.sleep()和await asyncio.sleep():前者阻塞整个线程,后者只挂起当前协程
目标站点响应慢或反爬,异步放大了失败率
异步不会让慢服务器变快,反而会让问题更集中爆发:连接拒绝、超时、302 跳转失败、SSL 错误批量出现。这不是代码写错了,而是并发压测暴露了真实瓶颈。
立即学习“Python免费学习笔记(深入)”;
- 加 retry 逻辑,但别无脑重试——用
aiohttp.RetryClient或手动 catchClientConnectorError、ServerDisconnectedError - 限制并发数别贪高,
limit=30比limit=100更稳;观察目标站返回的X-RateLimit-Remaining头动态调整 - UA、Referer、cookies 等 headers 要随 session 统一设置,别漏掉;有些站点对空 headers 直接 403


















