Python asyncio 事件循环演进:从手动管理到现代化实践

Python asyncio 事件循环演进:从手动管理到现代化实践

一文说清 asyncio.run()、get_event_loop() 与 get_running_loop() 的恩怨情仇


引言:异步编程的"循环"之惑

Python 的 asyncio 库让并发编程变得优雅,但它的 API 演进也带来不少困惑。很多初学者(甚至老手)都曾纠结过:

  • 启动协程到底该用 asyncio.run() 还是 loop.run_until_complete()?
  • get_event_loop() 和 get_running_loop() 长得这么像,该用哪个?
  • 为什么我的代码在 Python 3.10 里突然报警告了?

本文基于真实对话场景,系统梳理这些 API 的差异、历史背景和最佳实践。读完你将彻底告别事件循环的"选择困难症"。


第一部分:启动协程的"旧时代"------手动事件循环

在 Python 3.7 之前,运行一个异步函数需要三步曲:

  1. 通过 asyncio.get_event_loop() 获取一个事件循环对象。
  2. 调用循环的 run_until_complete(coro) 方法驱动协程执行。
  3. 手动调用 loop.close() 释放资源。
python 复制代码
import asyncio

async def main():
    await asyncio.sleep(1)
    print("Hello, asyncio!")

# 旧式写法(Python 3.6 及以前)
loop = asyncio.get_event_loop()
try:
    loop.run_until_complete(main())
finally:
    loop.close()

痛点:

  • 开发人员必须时刻记得 close(),否则可能造成文件描述符泄露。
  • 如果已有事件循环存在,get_event_loop() 可能返回一个"脏"循环,导致不可预知的行为。
  • 多线程场景下更容易出错。

第二部分:新时代的"瑞士军刀"------asyncio.run()

Python 3.7 正式引入了 asyncio.run(),它封装了上述所有繁琐步骤:

  • 总是创建一个全新的事件循环。
  • 运行传入的协程直到完成。
  • 自动关闭循环并清理资源。
  • 禁止在已有循环的环境中嵌套调用(抛出 RuntimeError),从根源上避免混乱。
python 复制代码
import asyncio

async def main():
    await asyncio.sleep(1)
    print("Hello, modern asyncio!")

# 现代推荐写法(Python 3.7+)
asyncio.run(main())

核心原则 :asyncio.run() 应该是所有异步程序的唯一顶层入口。除了它,不要再手动创建或获取事件循环。


第三部分:在协程内部,该用什么?

进入异步函数(async def)内部后,我们有时需要访问当前正在运行的事件循环(比如用于创建 Future 或 Timer)。这时就有两个"候选":

  • asyncio.get_running_loop()
  • asyncio.get_event_loop()

3.1 谁是谁?

函数 行为 无运行循环时
get_running_loop() 仅 返回当前线程中正在运行的事件循环 抛出 RuntimeError
get_event_loop() 返回当前策略下的事件循环,若不存在则自动创建一个 自动创建新循环(但会触发弃用警告)

3.2 为什么 get_event_loop() 被"抛弃"?

  • 隐式创建是祸根:在普通函数中调用它,可能会悄悄创建一个循环,而这个循环从未被运行或关闭,导致资源泄露。
  • 线程不确定性 :不同线程调用可能得到不同的循环,甚至返回 None。
  • 嵌套噩梦 :在 Jupyter Notebook 或某些 UI 框架中,已有一个循环在运行,此时调用 get_event_loop() 可能返回一个错误的循环,导致协程挂在"死循环"上。

官方判决:

  • Python 3.7:标记为 软弃用 (DeprecationWarning)
  • Python 3.10:转为 正式弃用 (PendingDeprecationWarning)
  • Python 3.12:在特定情况下抛出 DeprecationWarning,彻底建议迁移

3.3 正确示范:协程内部只用 get_running_loop()

python 复制代码
import asyncio

async def worker():
    # ✅ 正确:获取当前运行循环
    loop = asyncio.get_running_loop()
    # 执行一些需要 loop 的操作,比如创建定时器
    loop.call_later(1, lambda: print("Timer fired"))
    await asyncio.sleep(0.5)
    print("Worker done")

async def main():
    await worker()

asyncio.run(main())

如果在协程外部调用 get_running_loop(),会直接报错:

python 复制代码
# ❌ RuntimeError: no running event loop
loop = asyncio.get_running_loop()

第四部分:版本兼容性实战

如果你的项目仍需支持 Python 3.6(甚至更早),可以自行封装一个兼容函数,模拟 asyncio.run() 的行为:

python 复制代码
import asyncio
import sys

def run_coroutine(coro):
    if sys.version_info >= (3, 7):
        return asyncio.run(coro)
    # Python 3.6 及之前的备用方案
    loop = asyncio.new_event_loop()   # 强制新建,避免污染
    asyncio.set_event_loop(loop)
    try:
        return loop.run_until_complete(coro)
    finally:
        loop.close()

使用时:

python 复制代码
run_coroutine(main())

这样既能享受新版 API 的简洁,又能兼容旧版环境。


第五部分:一张决策表,从此不再纠结

使用场景 推荐 API 备注
顶层启动异步程序 asyncio.run(main()) 唯一正确入口,Python 3.7+
在异步函数内部获取当前循环 asyncio.get_running_loop() 安全、高效、无副作用
需要创建新循环(极少见) asyncio.new_event_loop() + set_event_loop() 仅在库开发或特殊需求时使用
旧版手动启动(已弃用) loop = get_event_loop(); loop.run_until_complete() 避免使用,除非兼容 ≤3.6
全局获取循环(已弃用) asyncio.get_event_loop() 从 3.10 起强烈不建议,3.12+ 警告

第六部分:常见陷阱与 FAQ

Q1:asyncio.run() 不能嵌套调用,那我要在已有循环里启动子协程怎么办?

A:直接用 await 调用即可,无需再套一层 run()。例如:

python 复制代码
async def sub(): ...
async def main():
    await sub()   # 正确
asyncio.run(main())

Q2:在 Jupyter Notebook 里用 asyncio.run() 报错?

A:因为 Notebook 本身已经运行着一个事件循环(如 Tornado)。你可以改用 await 直接在单元格中执行协程(需要 nest_asyncio 补丁),或使用 loop.run_until_complete() 但注意风险。

Q3:我想同时兼容新旧写法,但不想写兼容函数怎么办?

A:最简单的做法是强制要求 Python ≥3.7,因为 3.7 已经是 2018 年的版本,绝大多数环境都已升级。


结语:拥抱现代,告别混乱

Python 的异步生态一直在朝着"简洁、安全、明确"的方向演进。

  • asyncio.run() 是启动的大门,请把守好。
  • get_running_loop() 是内部的指南针,只在协程内使用。
  • 至于 get_event_loop()------请让它活在教科书里,而不是你的新代码里。

遵循这些最佳实践,你的异步代码将更加健壮、可维护,也更容易被他人理解。

相关推荐
bro_Java666几秒前
链表进阶2:双向链表的构建
java·数据结构·链表
DianSan_ERP22 分钟前
多平台订单自动下载与回传的技术实现:从消息推送到状态闭环引言
java·linux·服务器·前端·网络·架构·自动化
Joe_Wang524 分钟前
【从0到1学习JVM · 13】点进JDK源码只有一个分号?搞懂本地方法栈与JNI机制
java·jvm·学习·本地方法栈
编码者卢布31 分钟前
【App Service 】WebJobs 多实例实验:谁在运行,什么时候运行?
开发语言·python
quantdash_cc39 分钟前
Python 获取实时行情后如何进行批量筛选?从全市场快照到策略候选池
开发语言·python·数据分析·量化交易·股票数据·quantdash
流星。。方40 分钟前
网站做好了怎么上线?从域名解析到访问验证的完整实操
服务器
AI 算法大模型备案~当当43 分钟前
各地备案数量怎么看:一份属地公告的认读与台账方法
java·数据库·人工智能
智鸟科技GemeOpen开发者智能设备1 小时前
GemeOpen 智能音箱 GSSM0B - 播放控制(Java示例)
java·开发语言·智能音箱
小蜗 strong1 小时前
和电脑猜拳(随机程序应用)
服务器·前端·python
今天的砖头有点烫手啊1 小时前
Java IO/NIO/AIO 演进:从 BIO 到 Netty 的底层逻辑
java·jvm·nio