直接调用协程函数未await会触发RuntimeWarning,因协程对象未被驱动执行;须在async函数中用await,或在同步环境中用asyncio.run()调度,且不可在非async函数中使用await。

直接调用协程函数却没 await,报 RuntimeWarning: coroutine 'xxx' was never awaited
Python 协程函数返回的是一个协程对象(coroutine),不是普通函数,不能像 func() 这样直接调用完就结束。你看到的警告,本质是 Python 在垃圾回收时发现这个协程对象创建后既没 await 也没被 asyncio.run() 等驱动执行,就主动提醒你“漏了”。
常见触发场景:
- 在同步函数里写了
my_coro(),但没加await - 在
if分支或循环里构造了协程对象,但某些路径下没await - 误把协程当普通函数传给
map()、list comprehension等,结果只生成一堆未执行的coroutine对象
实操建议:
- 确认所在作用域是否为
async def函数内:如果是,必须用await my_coro();如果不是,得用asyncio.run(my_coro())或asyncio.create_task()等方式调度 - 检查缩进和条件逻辑——尤其注意
else分支或异常处理中是否遗漏await - 用
isinstance(my_coro(), types.CoroutineType)快速验证返回值类型(需先import types)
在非 async 函数里运行协程,必须用 asyncio.run() 而不是 loop.run_until_complete()
asyncio.run() 是 Python 3.7+ 推荐的顶层入口,它会自动创建新事件循环、运行协程、清理资源并关闭循环。而手动调用 loop.run_until_complete() 容易出问题,尤其在多次调用或已有运行中循环的环境里。
立即学习“Python免费学习笔记(深入)”;
典型错误现象:
- 第二次调用
loop.run_until_complete()报RuntimeError: This event loop is already running - 脚本退出后出现
ResourceWarning: unclosed transport - Jupyter 中反复执行单元格后协程卡住或不响应
实操建议:
- 脚本主入口一律用
asyncio.run(main()),别碰get_event_loop()和run_until_complete() - 如果真要复用循环(如嵌入到已有异步框架中),优先用
asyncio.create_task()把协程提交给当前运行的循环,而不是自己启停 - 避免在函数内部反复调用
asyncio.run()—— 它每次都会新建循环,开销大且可能冲突;应把多个协程合并成一个顶层协程再跑
await 只能在 async def 函数里用,否则报 SyntaxError: invalid syntax
这是语法层面限制:await 是关键字,解析器只允许它出现在 async def 定义的函数体中。哪怕只差一层缩进、少写一个 async,就会直接报错,连解释器都过不去。
容易踩的坑:
- 想在普通函数里写
await asyncio.sleep(1),结果 SyntaxError - 复制粘贴协程代码时漏掉开头的
async,或者把async def错打成def - 在类方法中用了
await,但忘记给方法加async(即使类本身继承自AsyncContextManager也不行)
实操建议:
- 编辑器开启 Python 语法高亮和 lint(如 Pylint、Ruff),这类错误能实时标红
- 不要试图“绕过”这个限制——比如用
exec()拼字符串、或用asyncio.run()包一层来模拟“同步 await”,这只会让逻辑更难追踪 - 若需在同步上下文中触发异步逻辑,明确拆成两层:上层同步封装(如返回
Future或启动任务),下层才是真正的async def
协程里调用阻塞函数(如 time.sleep、requests.get)会导致整个事件循环卡死
协程并发靠的是“让出控制权”,不是多线程。一旦在 async def 函数里调用纯同步阻塞操作,当前协程会一直占着事件循环不放,其他协程全得等着——表面是异步,实际串行,还失去并发优势。
典型表现:
- 多个
await asyncio.sleep(1)总耗时约 1 秒,但换成time.sleep(1)后变成 3 秒(3 个串行) - 用
requests.get()替代aiohttp.ClientSession.get(),QPS 不升反降
实操建议:
- 网络请求统一换
aiohttp、httpx.AsyncClient等异步库 - CPU 密集型操作(如 JSON 解析、加密)用
loop.run_in_executor()扔到线程池,避免阻塞事件循环 - 文件读写优先用
aiopath或anyio.Path,而不是内置open() - 调试时可在协程开头加
print(f"start at {time.time():.2f}"),对比time.sleep和asyncio.sleep的时间戳差异,快速定位阻塞点
协程不是魔法,它只解决 I/O 等待的并发问题。真正卡住循环的,永远是那些你以为“很快”、其实根本没让出控制权的调用。










