asyncio.run()不可在已有事件循环中调用,应改用get_event_loop().run_until_complete()或Jupyter中直接await;aiohttp.ClientSession须复用,配合Semaphore控制并发、显式捕获三类异常并合理重试。

asyncio.run() 不能在已运行的事件循环里调用
你写完 asyncio.run(main()) 运行协程,结果在 Jupyter、Django shell 或 Flask 开发服务器里直接报错:RuntimeError: asyncio.run() cannot be called from a running event loop。这是因为这些环境自己已经启了一个事件循环,再套一层会冲突。
- 开发时用
asyncio.get_event_loop().run_until_complete(main())替代asyncio.run() - Jupyter 中可直接用
await main()(前提是当前 cell 在 async 模式下) - 生产部署别手动启 loop,交给 Uvicorn / Hypercorn 等 ASGI 服务器管理
aiohttp.ClientSession 必须复用,不能每次请求都新建
看到“连接池”就以为自动优化了?错。aiohttp.ClientSession 自身带连接池,但如果你在 for 循环里反复 ClientSession() → session.get() → session.close(),不仅池失效,还会触发大量 TCP 握手和 TIME_WAIT,吞吐直接掉一半。
- 整个任务生命周期只创建一次
ClientSession,用async with session:包裹批量请求 - 需要自定义超时或 headers,统一在 session 初始化时传入
timeout=和headers=,别塞进每个session.get() - 不要在函数里 return session;它不是线程安全的,多协程共享没问题,但跨线程或跨 loop 就崩
requests + threading 并发 vs aiohttp + asyncio,并发量不是数字越大越好
1000 个请求,开 100 个线程 vs 开 1000 个协程,看起来后者更“高并发”,但实际可能更慢甚至失败——因为目标服务端有限流,本地也有文件描述符上限和 DNS 解析瓶颈。
- 用
asyncio.Semaphore(10)控制并发数,10~50 是较稳妥的起始值,根据目标响应时间和错误率动态调 - DNS 解析默认是阻塞的,加
connector = aiohttp.TCPConnector(limit=100, limit_per_host=30, use_dns_cache=True)避免卡住 - 别迷信“全异步”,如果请求中混有 CPU 密集操作(比如解析大 JSON、解密),得用
loop.run_in_executor()脱离事件循环,否则整个协程队列会被拖慢
异常处理必须覆盖 ConnectionError、ClientResponseError、TimeoutError 三类
try/except Exception: 看似保险,实则漏掉关键错误。aiohttp 的网络异常不继承 Exception 的常规链路,比如连接被拒是 aiohttp.ClientConnectorError,404/502 是 aiohttp.ClientResponseError,超时是 asyncio.TimeoutError —— 它们不会被 except Exception 捕获。
立即学习“Python免费学习笔记(深入)”;
- 显式 import 并分别捕获:
from aiohttp import ClientConnectorError, ClientResponseError、import asyncio - 对 4xx/5xx 响应,用
response.raise_for_status()触发ClientResponseError,比手动检查status >= 400更可靠 - 连接级错误(如 DNS 失败、目标宕机)往往需要重试,但得加退避:
asyncio.sleep(0.5 * (2 ** attempt)),避免雪崩
真正卡住人的从来不是语法,而是连接池没复用、异常类型没分清、并发数拍脑袋定。跑通一个协程容易,让 500 个请求稳定扛住目标限流和本地资源限制,得靠这几处细节抠到底。


















