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 之前,运行一个异步函数需要三步曲:
- 通过
asyncio.get_event_loop()获取一个事件循环对象。 - 调用循环的
run_until_complete(coro)方法驱动协程执行。 - 手动调用
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()------请让它活在教科书里,而不是你的新代码里。
遵循这些最佳实践,你的异步代码将更加健壮、可维护,也更容易被他人理解。