asyncio.await触发切换的条件是await一个尚未完成的awaitable,若对象已done则走快速路径零开销;强制切换如await asyncio.sleep(0),而await已完成task不实际挂起。

因为上下文切换本身不耗 CPU,但会累积调度延迟、内存分配和栈帧管理成本,在高频小任务中直接拖慢吞吐——它不是“能不能跑”,而是“能不能跑得稳又快”。
asyncio.await 触发切换的条件是什么?
不是“写了 await 就切”,而是“await 一个尚未完成的 awaitable”才真正挂起协程。CPython 的 PyCoro_Await 内部会先检查对象状态:如果 Future 已 done(),或 Task 已执行完毕,就跳过保存栈帧、不入等待队列,几乎零开销。
-
await asyncio.sleep(0)是强制切换,用于测试或让出控制权,生产环境慎用 -
await an_already_done_task(比如task = asyncio.create_task(coro()); await task)通常不触发实际调度 -
await asyncio.gather(a, b, c)只等结果,内部已批量注册,不是逐个 await - 但
await a; await b; await c是串行,多出两次调度决策 + 两次栈帧保存/恢复
哪些场景会让切换开销突然变大?
高频小 await 是典型放大器,比如每毫秒一次 Redis 查询、或在 for 循环里逐个 await process(item)。这时开销主要来自:
- 每次 await 都可能分配一个临时迭代器(
coro.__await__()返回值) - 事件循环要更新任务队列、判断优先级、重排就绪列表
- 协程栈帧反复压栈/出栈,影响 CPU 缓存局部性
- 若混用阻塞调用(如未用
asyncio.to_thread包装的json.loads),还会卡住整个事件循环
ContextVar 和上下文切换开销有关系吗?
没有。 ContextVar 解决的是“怎么让日志 trace_id、DB session、用户身份不丢”,不是“怎么少切换”。它的读写有微小开销,但不参与调度逻辑,也不减少或合并 await。误把它当性能工具,会导致两个问题:
- 在中间件或 ORM 中不用
ContextVar,协程恢复后拿不到原始请求上下文,出现数据错乱 - 为“省那几个纳秒”而放弃
ContextVar,换来的是更难 debug 的竞态 bug - 真想降开销,该盯
asyncio.gather批量并发、提前 resolve Future、用to_thread隔离阻塞,而不是动contextvars
最常被忽略的一点是:协程切换开销不是线性增长的。100 个串行 await 可能比 1 个 gather 慢 5 倍以上,但这个差距在监控指标里往往只体现为 P99 延迟毛刺——你得从调度行为本身去查,而不是看 CPU 或内存占用。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











