requests 在 asyncio 中会阻塞事件循环,因其底层为同步阻塞调用;应改用 aiohttp.clientsession 或通过 run_in_executor 隔离,避免卡死和性能退化。

不能直接调用,硬塞 requests.get() 会阻塞整个事件循环,异步代码瞬间退化成同步,100 个请求可能比单线程还慢。
为什么 requests 在 asyncio 里一用就卡死
requests 是纯同步库,底层调用 socket.connect()、ssl.wrap_socket() 等都是阻塞系统调用。一旦在协程里执行 requests.get("https://example.com"),当前协程会一直占着事件循环线程,其他协程全得干等——这和开 100 个线程跑 requests 没本质区别,还少了线程调度的灵活性。
- 现象:CPU 使用率低,但总耗时线性增长(比如 100 个请求 × 平均 300ms = 30 秒),
asyncio.gather完全失效 - 更糟的是:若某次请求卡在 SSL 握手或 DNS 解析,可能拖垮整批任务,且无法被
asyncio.wait_for()正确中断 -
requests.Session()复用连接也无效——它复用的是同步 TCP 连接,不是异步连接池
aiohttp.ClientSession 是唯一靠谱的替代方案
它原生基于 asyncio,所有 I/O 操作都注册到事件循环,连接复用、DNS 缓存、SSL 会话复用都按异步方式设计。别试图“包装” requests,直接换掉。
- 必须复用
ClientSession实例:在函数外创建,在协程中传入;重复创建 session 会泄漏连接、耗尽文件描述符 - 超时必须显式设:
timeout=ClientTimeout(total=10),否则默认无超时,一个挂起请求就能让整个协程组卡死 - SSL 验证可关但慎用:
connector=aiohttp.TCPConnector(ssl=False)能省 5–10% 时间,但仅限内网或测试环境 - 别漏掉
await response.text()或await response.json():不 await 就只是拿到一个ClientResponse对象,不是实际响应体
实在绕不开 requests?只能用 run_in_executor 做隔离
如果你必须调用某个只提供 requests 接口的 SDK,或依赖其 session 状态管理,唯一安全做法是把 requests 调用扔进线程池,和事件循环隔离开。
- 用
loop.run_in_executor(None, requests.get, url),不要自己建ThreadPoolExecutor(除非你明确要控并发数) - 注意:每次调用都会新建线程(
None表示用默认线程池),频繁调用仍可能触发 GIL 争抢,吞吐上限远低于 aiohttp - 异常处理要包两层:外层捕获
concurrent.futures.TimeoutError,内层捕获requests.exceptions.RequestException - 别在循环里反复调用
loop.run_in_executor启动新线程——线程创建开销大,应批量聚合后提交
asyncio.run() 和 get_event_loop().run_until_complete() 别混用
在已有 event loop 的上下文(如 Jupyter、FastAPI、uvicorn)里调用 asyncio.run() 会直接抛 RuntimeError: asyncio.run() cannot be called from a running event loop。
- 开发脚本用
asyncio.run(main())没问题;但嵌入 Web 框架或测试工具时,必须用asyncio.get_event_loop().run_until_complete(main()) - 如果不确定运行环境,优先查
asyncio.get_event_loop().is_running()再决定怎么启动 - 别在
async def函数里再调asyncio.run()——这是最常见也是最隐蔽的死锁来源
真正难的不是写 async/await,而是识别哪些调用真异步、哪些只是披着协程外衣的阻塞操作。requests 就是典型“假异步”,它的每个方法签名都像能 await,但实际执行时早把事件循环锁死了。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











