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)内部后,我们有时需要访问当前正在运行的事件循环(比如用于创建 FutureTimer)。这时就有两个"候选":

  • 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()------请让它活在教科书里,而不是你的新代码里。

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

相关推荐
Yanjun2i1 小时前
Agent学习记录三:完成 Agent Loop
python·学习·agent
abby1 小时前
美国多IP站群服务器Nginx配置与网络优化实操
服务器
郝学胜_神的一滴1 小时前
Effective Python 条款 8: 同时遍历多组序列:zip 与 zip_longest 的实战避坑指南
python·pycharm
盛世宏博智慧档案1 小时前
高速 ETC 门架外场机房温湿度远程监测解决方案
服务器·数据库·php·高度·高速·温湿度
承渊政道2 小时前
【Python编程—从入门到实践】(Python字典:从基础语义到现代工程实践)
开发语言·python·pycharm·字典·嵌套
学编程就要猛2 小时前
流式编程及Spring中SSE实现
java·后端·spring·流式编程
麻雀飞吧2 小时前
搜索“近期量化入门”时,先补交易理解再看 Python 和 API
人工智能·python
随遇而安zx2 小时前
【多线程】---ConcurrentHashMap 源码解析
java·多线程·并发
不才不才不不才2 小时前
Spring 源码系列(27): @Transactional 七大失效场景与源码归因
java·后端·spring