asyncio.run() 只能调用一次,因其每次创建并关闭新事件循环,已关闭的循环不可重启;多次调用会报 runtimeerror。await 普通函数直接 typeerror 报错,不卡住;真正阻塞的是同步调用如 time.sleep(),应改用 asyncio.sleep() 或 to_thread()。

asyncio.run() 为什么只能调用一次?
因为 asyncio.run() 内部会创建并关闭一个全新的事件循环,且 Python 不允许对已关闭的循环再次启动。这不是设计缺陷,而是明确的生命周期约束。
- 多次调用
asyncio.run()会触发RuntimeError: asyncio.run() cannot be called from a running event loop - 常见误用场景:在已进入
async def函数后,又试图用asyncio.run()启动子协程(比如想“重启”一个任务) - 正确做法是直接
await子协程,或用asyncio.create_task()提交到当前循环 - 如果真需要复用逻辑,把要跑的协程抽成函数,由外层统一
await或asyncio.run()
await 一个普通函数会卡住整个事件循环吗?
不会卡住,但会立刻报错:TypeError: object XXX can't be used in 'await' expression。Python 在语法层就拦截了这种写法,根本没机会执行。
-
await只接受awaitable对象:协程对象(async def返回值)、Future、实现了__await__的类 - 误把同步函数(如
time.sleep()、requests.get())直接await是新手高频错误,实际它连 awaitable 都不是 - 真正会“卡住”的是同步阻塞调用本身(比如在协程里调用
time.sleep(5)),这时整个事件循环停摆 5 秒 - 替代方案:用
asyncio.to_thread()(3.9+)或loop.run_in_executor()把同步操作扔进线程池
事件循环什么时候真正开始调度协程?
从 asyncio.run() 进入、或手动调用 loop.run_until_complete() 开始,事件循环才启动;在此之前,async def 只是定义协程函数,不产生任何调度行为。
-
async def不等于“自动并发”,它只是生成协程对象的语法糖,就像def只是定义函数对象 - 协程对象必须被显式提交给事件循环:通过
await(挂起当前协程,让出控制权)、asyncio.create_task()(立即加入调度队列)、或传给run_until_complete() - 未被 await 或 task 化的协程对象,就像未调用的函数,什么也不会发生
- 注意
asyncio.ensure_future()和create_task()的区别:后者必须在运行中的事件循环内调用,前者可接受协程对象并自动调度
为什么 asyncio.sleep() 不阻塞,而 time.sleep() 会?
因为 asyncio.sleep() 是协程函数,内部用 loop.call_later() 注册回调,不占用 CPU;time.sleep() 是系统调用,会让当前线程休眠,而 asyncio 默认只用一个线程运行事件循环。
-
asyncio.sleep(1):注册一个 1 秒后唤醒的回调,当前协程挂起,循环立刻去处理其他就绪任务 -
time.sleep(1):当前线程停 1 秒,所有协程都等——哪怕你开了 100 个 task,也全卡住 - 数据库、HTTP、文件读写等 I/O 操作同理:必须用 async 版本(如
aiohttp、aiomysql),否则一调用就拖垮整个循环 - 没有 async 版本的库?别硬套
await,老实用to_thread()包一层,至少不阻塞主线程
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











