asyncio.run()在flask/fastapi/jupyter中报错,因这些环境已启动事件循环,嵌套调用被python禁止;应改用await、create_task或gather等适配方案。

asyncio 本身不提升性能,它只帮你把已有的 IO 等待时间“腾出来”干别的事;用错地方反而更慢,比如在 CPU 密集任务里硬套 await。
为什么 asyncio.run() 在 Flask/FastAPI/Jupyter 里会报错
常见错误信息:RuntimeError: asyncio.run() cannot be called from a running event loop。因为 Flask(通过 Werkzeug)、FastAPI、Jupyter kernel 这些环境启动时已经自己调起了一个事件循环,你再调 asyncio.run() 就等于试图嵌套启动——Python 明确禁止。
- Web 框架中:直接写
async def路由函数(FastAPI 原生支持;Flask 需搭配flask-async或手动用asyncio.get_running_loop().create_task()) - Jupyter 中:别用
asyncio.run(),直接await main()(前提是 kernel 是 async-aware 的,如 IPython ≥8.0) - 命令行脚本:放心用
asyncio.run(main()),这是唯一推荐入口
哪些操作必须换异步库,否则就是伪异步
写了 async def 却还用同步阻塞调用,协程会在那卡死,整个事件循环被拖住。典型“假异步”行为:
- 网络请求:禁用
requests.get(),改用aiohttp.ClientSession或httpx.AsyncClient - 文件读写:禁用内置
open(),改用aiofiles.open() - 数据库:禁用
pymysql/psycopg2,改用asyncpg(PostgreSQL)、aiomysql(MySQL)或tortoise-orm - 延迟等待:禁用
time.sleep(1),必须用await asyncio.sleep(1)
想包装同步函数?只能走 loop.run_in_executor(),但要注意线程池默认大小是 min(32, os.cpu_count() + 4),高并发下容易打满,需显式传入自定义 concurrent.futures.ThreadPoolExecutor。
gather vs create_task:并发控制的关键分水岭
两者都并发执行,但调度时机和错误传播完全不同,选错会导致任务漏执行、异常吞没或意外串行。
- 全量依赖、等全部完成才继续:用
await asyncio.gather(task1(), task2(), task3());任一失败,整个gather抛异常 - 需要提前启动、中间穿插其他逻辑、或想单独处理每个任务结果:用
task = asyncio.create_task(task1()),之后await task或与其他任务组合 - 想忽略某个任务失败?加参数
return_exceptions=True,否则第一个异常就中断全部 - 切记:不要在 for 循环里反复
create_task()却不await或收集——任务对象不会自动销毁,内存持续增长,异常也无法捕获
真正影响吞吐的不是协程数量,而是最慢的那个 IO
如果你调用的下游服务 P99 延迟是 5 秒,哪怕你开了 10000 个协程、用了 uvloop、禁用了 GC,单个请求也绝不可能快于 5 秒。优化要从链路最慢环节入手:
- 加连接池限制(如
aiohttp.TCPConnector(limit=100)),避免瞬时打爆目标服务或触发限流 - 设超时(
timeout=aiohttp.ClientTimeout(total=10)),防止一个慢请求拖垮整批 - 生产必须用
uvloop:只需两行 ——import uvloop+asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()),实测提升 2–4 倍调度效率 - 系统级调优不能少:
ulimit -n 1048576防EMFILE,否则连不上就谈不上并发
最容易被忽略的是:asyncio 的“快”,永远建立在 IO 真正可异步的前提下;一旦混入同步调用、CPU 密集计算或未设限的并发冲击,它立刻退化成比线程池还难 debug 的黑盒。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











