python 3.12 中 async 代码必须精准编写才能避免延迟飙升:需用长生命周期事件循环替代 asyncio.run()、并发 i/o 必用 asyncio.gather()、httpx 超时须分层配置、数据库必须用原生异步驱动(如 asyncpg/aiomysql)并复用连接池。

asyncio 在 Python 3.12 中不是“能用”,而是**必须用对才不拖慢延迟**。3.12 的事件循环调度开销已压到 850ns 级别,但若写法错一步,实际延迟可能翻 5 倍——不是 CPU 慢,是协程被卡在不该等的地方。
asyncio.run() 启动方式直接决定首字节延迟
Python 3.12 默认启用 asyncio.Runner(替代旧式 get_event_loop() + run_until_complete()),但多数人仍用 asyncio.run(main())。这看似无害,实则每次调用都重建事件循环、重置所有内部状态,冷启动延迟高 15–20ms。
微服务场景下应改用长生命周期事件循环:
- Uvicorn/FastAPI 等 ASGI 服务器已自动管理循环,无需手动
run() - 若需自建 HTTP 服务(如用
aiohttp.web),用asyncio.get_event_loop().run_forever(),并在进程启动时预热一次asyncio.sleep(0)触发调度器初始化 - 避免在请求处理函数里嵌套调用
asyncio.run()—— 这会抛RuntimeError: asyncio.run() cannot be called from a running event loop
await 顺序错误会让 P99 延迟暴涨
常见误区:把多个异步 I/O 当成“可并行”就自然并发。实际上,await a(); await b() 是串行;而 await asyncio.gather(a(), b()) 才是并发执行。
低延迟服务中,每个请求常需聚合缓存、DB、外部 API 三路数据。错误写法会让总延迟 = sum(各路耗时),正确写法接近 max(各路耗时):
- 用
asyncio.gather()并发发起所有 I/O,而非逐个await - 慎用
asyncio.as_completed()—— 它返回迭代器,需async for消费,额外引入调度开销,P99 延迟比gather高约 3–5ms - 若某路 I/O 可选(如降级查本地缓存),用
asyncio.wait_for(task, timeout=50e-3)主动设上限,防止慢依赖拖垮整体
HTTPX 超时配置必须分层,不能只设 total
httpx.AsyncClient 的默认超时是 5 秒 total,这对微服务太危险:连接失败本该 200ms 内感知,却要等满 5 秒才 fallback,P99 直接崩掉。
Python 3.12 下推荐显式拆解超时:
-
connect=1.0:TCP 握手超时,公网服务建议 ≤ 1s,内网可压到 200ms -
read=3.0:从 socket 读响应体的上限,比 connect 更关键,因网络抖动易卡在此处 -
pool=100e-3:从连接池取空闲连接的等待时间,设太大会阻塞后续请求,设太小又频繁建连 - 禁用
timeout=None或timeout=math.inf—— 3.12 的事件循环不会帮你兜底,协程会永远挂起
数据库驱动必须用 asyncpg / aiomysql,psycopg2 不行
psycopg2 是同步驱动,哪怕包在 loop.run_in_executor() 里,也会触发线程切换和 GIL 争抢,实测在 3.12 下单请求延迟增加 8–12ms,且并发 > 200 时线程池排队严重。
真正零拷贝、零线程切换的路径只有一条:
- PostgreSQL 用
asyncpg(非aiopg,后者是 psycopg2 封装) - MySQL 用
aiomysql或mysql-connector-python的 async 模式(需确认版本 ≥ 8.0.33) - 连接池必须复用:
asyncpg.create_pool()初始化一次,全局共享,避免每次请求新建连接 - 注意
asyncpg的fetchrow()和fetchval()比fetch()快 15% —— 因少做结果集封装
async 函数,是判断哪一行该 await、哪一行该提前 gather、哪一行该用 wait_for 切断。Python 3.12 的底层优化再猛,也救不了逻辑上串行的协程。Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











