asyncio.run() 不能在已有事件循环中调用,因其专为顶层脚本设计,会新建并管理完整事件循环;在jupyter、fastapi等已托管循环的环境中应改用await或create_task()。

asyncio.run() 为什么不能在已有事件循环里调用
直接调用 asyncio.run() 会检查当前线程是否已有运行中的事件循环,有就抛出 RuntimeError: asyncio.run() cannot be called from a running event loop。这是设计使然,不是 bug —— 它专为“脚本顶层”启动设计,内部会新建、运行、关闭整个循环。
常见错误场景:在 Jupyter Notebook、FastAPI 的依赖函数、或已用 asyncio.get_event_loop().run_until_complete() 启动的协程里再调用 asyncio.run()。
- Web 框架(如 FastAPI/Starlette)已托管事件循环,直接写
asyncio.run(some_coro())必报错 - 想复用当前循环?改用
await some_coro()或asyncio.create_task() - 需要从同步上下文“挤进去”?用
asyncio.get_event_loop().create_task()(Python 3.7+),但注意返回的是Task对象,不是结果
asyncio.gather() 和 asyncio.create_task() 的分工差异
asyncio.gather() 是批量并发执行并等待全部完成的“协调器”,适合任务彼此独立、你只关心最终结果集合;asyncio.create_task() 是把协程“提交给循环马上调度”的底层操作,适合需要精细控制生命周期(比如中途取消、加回调、或提前 await 其中某一个)。
性能上没本质差别,但误用会导致逻辑错误:
- 反复对同一协程对象调用
asyncio.gather(coro, coro)会报RuntimeError: cannot reuse already awaited coroutine - 用
asyncio.create_task()后忘了 await,任务可能被调度但结果丢弃,甚至因异常未捕获而静默失败 - 大量 IO 任务(如 HTTP 请求)优先用
gather();需要动态增删任务或响应式 await 某个子任务时,用create_task()+asyncio.wait()或asyncio.as_completed()
await time.sleep() 是性能杀手,必须换 asyncio.sleep()
同步的 time.sleep() 会阻塞整个事件循环,哪怕只睡 1 秒,所有其他协程都卡住 —— 这和写同步代码没区别,完全浪费异步能力。
典型错误现象:写了 10 个并发请求,总耗时却接近单个请求 × 10,而不是接近单个最长请求。
- 所有延时操作必须用
asyncio.sleep() - 第三方库若只提供同步 sleep(比如某些 SDK 的重试逻辑),得自己包一层:
await asyncio.to_thread(time.sleep, 1)(Python 3.9+),但这是退化方案,尽量找原生异步接口 - 数据库驱动、HTTP 客户端(如
aiohttp、httpx.AsyncClient)必须用异步版本,否则一调用就阻塞
async/await 函数里混用同步阻塞调用的后果
在 async def 函数里直接调用 requests.get()、json.load()(读大文件)、或正则匹配超长文本,会把事件循环“钉死”。协程看似是异步声明,但执行体仍是同步的。
这不是语法错误,而是隐蔽的性能陷阱:
- CPU 密集型操作(如解析 GB 级 JSON、加密计算)应移到线程池:
await asyncio.to_thread(json.loads, data) - 文件 IO 不要用
open().read(),改用asyncio.to_thread()或aiofiles库 - 没有
await的循环、递归、正则回溯,照样阻塞 —— 异步不是魔法,它只在await点让出控制权
真正难的不是写 async,是识别哪些调用不可“让出”。很多性能问题根源不在 asyncio 本身,而在没把阻塞点真正剥离出去。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











