await 不是自动并发,而是显式让出执行权;它仅在遇到可等待对象时暂停当前协程并交还控制权给事件循环,真正并发需依赖非阻塞I/O或已注册任务。

await 不是“自动并发”,而是显式让出执行权
很多人以为只要加了 await,函数就会“自动并行跑起来”,结果发现程序还是卡在某个 await 后面半天没反应——其实 await 本身不触发并发,它只是告诉事件循环:“我现在要等这个东西,你先去干别的”。真正并发的前提是:被 await 的对象本身是可等待的(比如 asyncio.sleep()、await response.json()),且背后调用了非阻塞 I/O 或已注册到事件循环的任务。
常见错误现象:await time.sleep(1) 直接阻塞整个协程(time.sleep 是同步阻塞函数);或对普通函数返回值盲目 await,报 TypeError: object xxx can't be used in 'await' expression。
- 用
asyncio.sleep()替代time.sleep() - 确保
await的是协程对象、Awaitable实例或实现了__await__的对象 - 不确定时,先用
inspect.iscoroutine()或asyncio.iscoroutine()检查
哪些地方必须 await?漏掉就等于“假异步”
漏 await 是 Python 异步代码最隐蔽的性能杀手。表面上程序能跑通,但实际逻辑串行执行,CPU 控制权没交出去,其他任务全被饿死。
典型场景:调用一个返回 Coroutine 的函数却忘了 await,比如 fetch_data() 返回协程,写成 result = fetch_data() 而不是 result = await fetch_data(),此时 result 就是个未执行的协程对象,后续可能报错或静默失效。
- 所有返回协程的函数调用都必须
await,包括你自己写的async def函数 -
asyncio.create_task()后不用await,但asyncio.gather()、asyncio.wait()必须await - 用
async for遍历异步迭代器时,底层每次迭代都隐式await,但别手动再套一层await
主动让出控制权的合理时机:不是越多越好
频繁插入 await asyncio.sleep(0) 确实能让出控制权,但滥用会拖慢单个任务,还可能放大调度开销。它只在明确需要“让其他协程插队”的地方才必要,比如长循环中防止独占事件循环。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
使用场景:解析大文件时逐行处理、实时日志轮询、状态轮训但不想阻塞网络请求等。
- 纯 CPU 密集型工作(如大数组计算)不适合靠
await解决,应改用loop.run_in_executor() -
await asyncio.sleep(0)是最轻量的让出方式,比await asyncio.sleep(0.001)更高效 - 在
while True:循环里每几轮加一次await asyncio.sleep(0),比每次循环都加更合理
为什么有时候 await 之后还是卡住?检查这三点
响应延迟不一定是代码问题,更可能是底层依赖没走异步路径。比如用了同步数据库驱动、HTTP 客户端没换异步版、或者第三方库内部偷偷调了阻塞系统调用。
常见错误现象:await httpx.AsyncClient().get(...) 没问题,但换成 requests.get(...) 就卡死;或用 sqlite3 直接读写本地数据库,哪怕包在 async def 里也阻塞。
- 确认 I/O 库是否真正异步:
aiohttp、httpx(带 AsyncClient)、aiomysql、asyncpg等 - 避免在协程中直接调用任何带
time.sleep、subprocess.run、open()(未配asyncio.to_thread)的操作 - 用
asyncio.to_thread()包裹无法修改的同步调用(Python ≥3.9),而不是硬扛着不 await
真正的难点不在语法,而在于分清“谁在控制 CPU”——事件循环只在 await 点调度,其余时间完全由当前协程支配。这点一旦混淆,再多的 async 关键字也救不了响应性。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










