asyncio.run()内调用同步函数会阻塞事件循环,导致协程暂停;await普通函数会静默忽略;在web框架或测试中滥用asyncio.run()引发性能与冲突问题;混合调用需注意异常传播与任务管理。

asyncio.run() 里直接调用同步函数会阻塞整个事件循环
同步函数一旦执行,就会卡住当前线程,而 asyncio 的事件循环就跑在这个线程上。哪怕只是一次 time.sleep(1) 或读取小文件的 open().read(),也会让所有待处理的协程停摆——不是“慢”,是彻底暂停。
常见错误现象:asyncio.run() 执行时间远超预期,监控显示 CPU 占用低但响应停滞;多个 async def 函数并发启动后,实际是串行执行。
- 别在
async函数里直接写requests.get()、json.load()、subprocess.run() - 必须用
loop.run_in_executor()包装耗时同步调用,或改用异步等价库(如aiohttp、aiosqlite) - 注意:
run_in_executor默认使用concurrent.futures.ThreadPoolExecutor,CPU 密集型任务应换ProcessPoolExecutor,否则无法释放 GIL
await 一个普通函数会报 RuntimeWarning: coroutine 'xxx' was never awaited
这是 Python 解释器在提醒你:你写了 await func(),但 func 根本不是协程函数(没加 async def),返回的是普通值,await 对它无效,且不会报错——只是静默忽略,逻辑可能完全错乱。
使用场景:调试时看到协程没执行、变量始终是 None 或旧值,但又没抛异常,大概率是这里漏了 async 声明。
立即学习“Python免费学习笔记(深入)”;
- 检查被
await的对象类型:print(type(func())),协程应为<class></class> - 函数定义必须是
async def func():,不能是def func():即使里面用了await - 第三方库函数(如
redis.Redis.get())默认同步,需确认是否提供aioredis等异步版本
在同步上下文里调用 async 函数不报错但会出事
比如在 Django 视图、Flask 路由、或单元测试的 setUp() 中直接写 asyncio.run(my_async_func()),表面能跑通,但每次调用都新建+关闭事件循环,开销大、状态不复用,还可能和已有循环冲突(如 Jupyter、uvicorn 已启动了自己的 loop)。
性能影响:单次 asyncio.run() 启动成本约 0.1–0.5ms,高频调用下不可忽视;更严重的是,嵌套调用可能触发 RuntimeError: asyncio.run() cannot be called from a running event loop。
- Web 框架要选原生支持异步的(FastAPI、Starlette),避免在 Flask/Django 同步视图里硬塞
asyncio.run() - 测试中优先用
pytest-asyncio插件 +@pytest.mark.asyncio,而非手动asyncio.run() - 若必须桥接,用
asyncio.get_event_loop().create_task()替代run(),但得确保 loop 正在运行
混合调用时的异常传播容易断层
同步代码抛出的异常(如 ValueError)进到 run_in_executor 后,会包装成 concurrent.futures.CancelledError 或丢失原始 traceback;而协程里未捕获的异常,可能被事件循环吞掉,只在日志里留一行 Task exception was never retrieved。
可观察现象:程序没崩溃,但某次请求没返回、数据库没写入、也没任何错误日志。
- 对
run_in_executor的结果,务必用await并包try/except,不要依赖外层捕获 - 给每个
create_task()加asyncio.create_task(..., name="xxx"),方便定位失败任务 - 全局设置
asyncio.get_event_loop().set_exception_handler(),至少把未处理异常打出来
真正难的不是写对语法,而是判断哪段 IO 必须异步化、哪段可以接受同步阻塞、以及当两者不得不共存时,异常和生命周期怎么兜住——这些边界往往藏在日志不报错、压测才暴露、上线后偶发失效的地方。










