asyncio 各版本有哪些重要变化?老代码怎么迁移?
简化版
asyncio 是标准库里演进最快的模块之一,网上大量教程停留在 Python 3.5~3.6 的写法,照抄会踩一堆弃用警告甚至运行时错误。要记住的关键节点:Python 3.7 是分水岭——引入了 asyncio.run()(从此不用手动管理事件循环)、asyncio.create_task()(替代 ensure_future)、get_running_loop()(替代语义混乱的 get_event_loop()),还有 contextvars(异步上下文传递)。3.8 把 CancelledError 改成继承 BaseException(except Exception 从此抓不到取消,这是有意设计),并加入 AsyncMock。3.9 加入 asyncio.to_thread()(比 run_in_executor 简洁且会传递 contextvars)。3.10 移除了所有 API 的 loop= 参数(老代码传 loop=loop 会直接报错)。3.11 是第二个大版本:asyncio.TaskGroup(结构化并发)、asyncio.timeout()(上下文管理器形式的超时)、ExceptionGroup 和 except*,外加大幅性能优化。3.12 起 get_event_loop() 和整个 EventLoopPolicy 体系进入弃用流程(3.16 计划移除),同时 asyncio.run 支持 loop_factory 参数、新增 asyncio.Runner 用于复用 loop。3.13 加入 Queue.shutdown() 和 eager task factory。核心记忆:3.7 有了 asyncio.run、3.8 CancelledError 变 BaseException、3.10 移除 loop=、3.11 有了 TaskGroup/timeout、3.12+ 弃用 get_event_loop。
详细版
版本速查表:
| 版本 | 关键变化 |
|---|---|
| 3.7 | asyncio.run()、create_task()、get_running_loop()、contextvars |
| 3.8 | CancelledError 继承 BaseException、AsyncMock、Windows 默认 Proactor |
| 3.9 | asyncio.to_thread()、loop.shutdown_default_executor() |
| 3.10 | 移除所有 loop= 参数、get_event_loop 无运行 loop 时告警 |
| 3.11 | TaskGroup、asyncio.timeout()、ExceptionGroup/except*、性能大幅优化 |
| 3.12 | 弃用 get_event_loop/EventLoopPolicy、asyncio.Runner、run(loop_factory=) |
| 3.13 | Queue.shutdown()、eager task factory、free-threading 实验支持 |
| 3.14 | 进一步清理弃用项、TaskGroup 等 API 完善 |
import asyncio, sys
# ① ★3.7 之前 vs 之后(最常见的老写法)★
# ✗ 老写法(3.4~3.6 教程里到处都是)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()
# ✓ 3.7+
asyncio.run(main())
# ✗ 老:asyncio.ensure_future(coro)
# ✓ 新:asyncio.create_task(coro) ★只能在协程内调用★
# ✗ 老:loop = asyncio.get_event_loop() (★在协程里也这么写★)
# ✓ 新:loop = asyncio.get_running_loop() ★没有运行的 loop 会直接报错(语义清晰)★
# ② ★3.8:CancelledError 变成 BaseException★
try:
await work()
except Exception: # ★抓不到 CancelledError 了!★
...
# ✓ 正确写法
try:
await work()
except asyncio.CancelledError:
await cleanup()
raise # ★必须重新抛出★
except Exception:
logging.exception("业务失败")
# ③ ★3.9:to_thread★
# ✗ 老:await loop.run_in_executor(None, blocking_fn, arg)
# ✓ 新:await asyncio.to_thread(blocking_fn, arg) ★还会传递 contextvars★
# ④ ★3.10:loop 参数被移除★
# ✗ asyncio.Queue(loop=loop) → TypeError
# ✗ asyncio.sleep(1, loop=loop) → TypeError
# ✓ 直接不传(自动用当前运行的 loop)
# ⑤ ★3.11:TaskGroup 和 timeout★
# ✗ 老:await asyncio.gather(a(), b()) # 失败时其余任务仍在跑
# ✓ 新:
async with asyncio.TaskGroup() as tg:
tg.create_task(a())
tg.create_task(b()) # ★一个失败会取消其余 + 保证退出时都结束★
# ✗ 老:await asyncio.wait_for(work(), timeout=5)
# ✓ 新(更灵活,可包多条语句):
async with asyncio.timeout(5):
await step1()
await step2()
# ⑥ ★3.12+:不要再用 get_event_loop / policy★
# ✗ asyncio.get_event_loop() → DeprecationWarning
# ✗ asyncio.set_event_loop_policy(...) → 3.12 起弃用,3.16 移除
# ✗ uvloop.install() → 内部就是 policy,已弃用
# ✓ asyncio.run(main(), loop_factory=uvloop.new_event_loop)
# ✓ with asyncio.Runner() as runner: runner.run(coro) ★复用 loop★
# ⑦ 兼容多版本的写法
if sys.version_info >= (3, 11):
async with asyncio.timeout(5):
await work()
else:
await asyncio.wait_for(work(), timeout=5)
⚠️ 三个最容易踩的版本坑:①
CancelledError从 3.8 起继承BaseException——这意味着except Exception:再也抓不到任务取消。这是有意的设计:防止业务代码里宽泛的异常处理把取消信号误吞掉(一旦吞掉且不重新抛出,任务就变成「不可取消」,优雅关闭会直接超时)。但从 3.7 迁移过来的代码如果依赖except Exception来做清理,行为会静默改变。② 3.10 移除了所有 API 的loop=参数——asyncio.Queue(loop=loop)、asyncio.sleep(1, loop=loop)、asyncio.Lock(loop=loop)这类老写法会直接抛TypeError。原因是这些对象本来就应该绑定到「运行时的那个 loop」,显式传 loop 只会制造跨 loop 使用的 bug。③ 3.12 起asyncio.get_event_loop()和整个EventLoopPolicy体系进入弃用流程(3.16 计划移除)——get_event_loop()的语义长期混乱(有时创建新 loop、有时返回已有的、在没有运行 loop 时行为随版本变化)。协程内部一律用asyncio.get_running_loop(),需要自定义 loop 用asyncio.run(main(), loop_factory=...)或asyncio.Runner。
完整版教学
一、3.7:asyncio 的分水岭
★ Python 3.7 引入的四件大事:
① ★asyncio.run()★ —— 最重要的改变
3.7 之前(★网上老教程的标准写法★):
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(main())
finally:
loop.close()
3.7+:
asyncio.run(main())
★ asyncio.run 做的事(★比手写的完整★):
- 创建新的事件循环
- 运行主协程
- ★取消所有剩余任务并等它们结束★
- ★关闭异步生成器(shutdown_asyncgens)★
- ★关闭默认 executor(3.9+)★
- 关闭 loop
→ ★手写版本几乎都会漏掉后面几步★
② ★asyncio.create_task()★
老:asyncio.ensure_future(coro) / loop.create_task(coro)
新:asyncio.create_task(coro) ★只能在协程内调用(更安全)★
★ ensure_future 仍然存在,但它是"万能转换器"(协程/Future/awaitable 都吃),
语义模糊 → ★明确要创建任务时用 create_task★
③ ★get_running_loop()★
老:asyncio.get_event_loop()
→ ★语义混乱★:在协程里返回当前 loop;不在协程里可能★创建一个新的★;
主线程/子线程行为还不一样
新:asyncio.get_running_loop()
→ ★没有运行中的 loop 就直接 RuntimeError★(语义明确)
✓ 判断"当前是否在异步上下文"的标准写法:
try:
asyncio.get_running_loop(); in_async = True
except RuntimeError:
in_async = False
④ ★contextvars(PEP 567)★
→ 解决"异步上下文传递"(threading.local 在协程间会串)
→ asyncio 的 Task 创建时会自动复制上下文快照
★ 其他 3.7 变化:
- async / await 成为★保留关键字★(不能再当变量名)
- asyncio.current_task() / all_tasks()(替代 Task.current_task())
- loop.start_tls()
★ 迁移信号:看到这些就是 3.7 之前的老代码
✗ get_event_loop() + run_until_complete()
✗ ensure_future
✗ @asyncio.coroutine + yield from(★3.8 弃用、3.11 移除★)
✗ Task.current_task() / Task.all_tasks()
Python 3.7 是 asyncio 的分水岭,四件大事:① asyncio.run()——从此不用手动管理事件循环,而且它做的事比手写的完整得多(取消剩余任务、关闭异步生成器、关闭默认 executor,手写版本几乎都会漏掉);② asyncio.create_task() 替代语义模糊的 ensure_future(后者是「万能转换器」,协程、Future、awaitable 都吃);③ get_running_loop() 替代语义混乱的 get_event_loop()(后者在协程里返回当前 loop、不在协程里可能创建新的、主线程和子线程行为还不一样),新函数在没有运行 loop 时直接报错,语义明确——这也成了「判断当前是否在异步上下文」的标准写法;④ contextvars 解决异步上下文传递。看到 get_event_loop() + run_until_complete()、ensure_future、@asyncio.coroutine + yield from(3.11 已移除)这些写法,就说明是 3.7 之前的老代码。
二、3.8~3.10:语义收紧与 API 清理
★ 3.8:CancelledError 的重大语义变化★
★CancelledError 从 Exception 改为继承 BaseException★
影响:
try:
await work()
except Exception: # ★3.7 能抓到取消,3.8+ 抓不到★
cleanup()
→ 这是★有意设计★:
取消是"控制流信号"而不是"错误",不应该被业务的兜底 except 吞掉
★一旦吞掉且不重新抛出,任务就"拒绝取消"★ → 优雅关闭超时
✓ 正确模式:
try:
await work()
except asyncio.CancelledError:
await cleanup()
raise # ★必须★
except Exception:
logging.exception(...)
★ 3.8 的其他变化:
- ★AsyncMock★(unittest.mock)→ 异步测试终于有官方支持
- ★Windows 默认改为 ProactorEventLoop★(支持子进程)
- asyncio.run 的 debug 参数
- ★@asyncio.coroutine 装饰器弃用★(3.11 移除)
- IsolatedAsyncioTestCase(标准库的异步测试基类)
★ 3.9:
- ★asyncio.to_thread()★ ← ★最实用的新增★
老:await loop.run_in_executor(None, fn, arg)
新:await asyncio.to_thread(fn, arg)
★ 差别不只是简洁:to_thread ★会传递 contextvars★,run_in_executor 不会
- loop.shutdown_default_executor()(asyncio.run 会自动调用)
- asyncio.run 会关闭默认线程池
★ 3.10:★清理 loop 参数★
移除了所有 asyncio API 的 loop= 参数:
✗ asyncio.Queue(loop=loop)
✗ asyncio.Lock(loop=loop) / Event / Semaphore / Condition
✗ asyncio.sleep(1, loop=loop)
✗ asyncio.gather(*coros, loop=loop)
✗ asyncio.wait(fs, loop=loop)
→ 全部 ★TypeError: got an unexpected keyword argument 'loop'★
★ 原因:这些对象应该绑定到"运行时的那个 loop",
显式传 loop 只会制造★跨 loop 使用★的 bug
✓ 迁移:直接删掉 loop= 参数
其他:
- get_event_loop 在★没有运行 loop 时★发 DeprecationWarning
- asyncio.iscoroutinefunction 支持 functools.partial
★ 这三个版本的共同主题:★收紧语义、消除歧义★
→ 老代码升级时最容易在这三处出问题:
CancelledError 的捕获、loop= 参数、get_event_loop 的用法
3.8~3.10 的主题是收紧语义、消除歧义。3.8 最重要的是 CancelledError 改为继承 BaseException——这是有意设计:取消是「控制流信号」而不是「错误」,不该被业务的兜底 except Exception 吞掉(一旦吞掉且不重新抛出,任务就「拒绝取消」,优雅关闭会超时);同期还有 AsyncMock、Windows 默认改为 Proactor、@asyncio.coroutine 弃用。3.9 最实用的是 asyncio.to_thread()——它不只是 run_in_executor 的简写,关键区别是它会传递 contextvars(trace_id 能带进线程)。3.10 清理了所有 API 的 loop= 参数(Queue(loop=)、sleep(1, loop=)、gather(..., loop=) 全部报 TypeError),原因是这些对象本就该绑定到运行时的 loop,显式传只会制造跨 loop 的 bug。老代码升级时最容易在这三处出问题:CancelledError 的捕获、loop= 参数、get_event_loop 的用法。
三、3.11:第二个大版本
★ 3.11 是继 3.7 之后最重要的 asyncio 版本,三个新特性 + 大幅提速:
① ★asyncio.TaskGroup(结构化并发)★
async with asyncio.TaskGroup() as tg:
tg.create_task(a())
tg.create_task(b())
# ★退出时保证两个任务都已结束★
相比 gather 的四个优势:
- ★一个失败会取消其余任务★(gather 不会,其余继续跑)
- ★退出块时保证没有遗留任务★(结构化)
- ★自己持有强引用★(不用担心任务被 GC)
- ★异常汇总成 ExceptionGroup★(不是只抛第一个)
→ ★3.11+ 应该默认用它替代 gather★
(只有"部分失败可接受、要全部跑完"才用 gather(return_exceptions=True))
② ★asyncio.timeout() / timeout_at()(上下文管理器形式的超时)★
老:await asyncio.wait_for(coro(), timeout=5) # ★只能包一个协程★
新:
async with asyncio.timeout(5):
await step1()
await step2() # ★整段共享一个超时★
★ 超时时抛 TimeoutError(3.11 起 asyncio.TimeoutError 就是内置的 TimeoutError)
★ 还能动态调整:
async with asyncio.timeout(5) as cm:
cm.reschedule(loop.time() + 10) # ★延长★
③ ★ExceptionGroup + except*(PEP 654)★
TaskGroup 失败时抛的就是 ExceptionGroup
try:
async with asyncio.TaskGroup() as tg: ...
except* ValueError as eg:
... # ★eg 是"只含 ValueError 的子组"★
except* TimeoutError as eg:
... # ★多个 except* 分支都可能执行★
④ ★性能优化(★常被忽略但很实在★)★
- 零成本异常处理(try 块不抛异常时★没有运行时开销★)
- 更快的解释器(PEP 659 专用化)
- asyncio 内部实现优化
→ 实测异步程序整体提速可观(★不用改一行代码★)
★ 3.11 还有:
- asyncio.Barrier
- TaskGroup 相关的 Task.uncancel()
- ★移除 @asyncio.coroutine 和 yield from 风格★(3.8 弃用 → 3.11 删除)
★ 一个重要的连锁影响:
★asyncio.TimeoutError 现在是内置 TimeoutError 的别名★
→ except asyncio.TimeoutError 和 except TimeoutError 等价
→ 老代码里两者分开写的地方可以合并
3.11 是继 3.7 之后最重要的 asyncio 版本。① TaskGroup(结构化并发) 相比 gather 有四个优势:一个失败会取消其余任务、退出块时保证没有遗留任务、自己持有强引用、异常汇总成 ExceptionGroup——3.11+ 应该默认用它替代 gather。② asyncio.timeout() 是上下文管理器形式的超时,比 wait_for 更灵活(可以包住多条语句共享一个超时预算,还能用 cm.reschedule() 动态延长)。③ ExceptionGroup + except* 用于处理 TaskGroup 抛出的多个异常。④ 性能优化常被忽略但很实在——零成本异常处理(try 块不抛异常时没有运行时开销)加上 PEP 659 的专用化解释器,不用改一行代码就能提速。还有一个连锁影响要记住:asyncio.TimeoutError 从 3.11 起就是内置 TimeoutError 的别名,两者可以合并写。
四、3.12~3.14:弃用清理与新工具
★ 3.12 的核心主题:★清理事件循环的管理方式★
① ★弃用 get_event_loop 和 EventLoopPolicy★
asyncio.get_event_loop() → ★没有运行 loop 时 DeprecationWarning★
asyncio.set_event_loop_policy() → ★弃用,3.16 计划移除★
asyncio.get_event_loop_policy() → 同上
★ 连带影响:uvloop.install() 内部用的就是 policy → ★也被弃用★
✓ 新的做法:
asyncio.run(main(), loop_factory=uvloop.new_event_loop) # ★3.12+★
# 或
with asyncio.Runner(loop_factory=...) as runner:
runner.run(coro1())
runner.run(coro2()) # ★复用同一个 loop★
② ★asyncio.Runner(3.11 引入、3.12 完善)★
解决 "asyncio.run 每次新建销毁 loop" 的问题:
with asyncio.Runner() as runner:
r1 = runner.run(task1())
r2 = runner.run(task2()) # ★同一个 loop → 连接池等资源可复用★
→ 适合"同步主程序里多次调用异步代码"的场景
③ 其他:
- asyncio.eager_task_factory(★3.12★):任务创建时★立即同步执行到第一个 await★
→ 对"大部分任务能同步完成"的场景能省下调度开销
loop.set_task_factory(asyncio.eager_task_factory)
- ★移除了一批 3.8 起弃用的 API★
★ 3.13:
- ★asyncio.Queue.shutdown()★ —— 终于有了优雅关闭队列的标准方式
q.shutdown() # 后续 put/get 抛 QueueShutDown
q.shutdown(immediate=True) # ★丢弃未处理的项★
→ ★以前要靠哨兵值(None)★,现在有官方支持
- ★自由线程(no-GIL)实验支持★
- 性能继续优化、TaskGroup 相关修复
★ 3.14:
- 继续清理弃用项(★policy 相关走向移除★)
- asyncio 内部实现优化
- ★free-threading 转为官方支持(仍非默认)★
★ 弃用时间线(★答题时能说出趋势就够★):
┌────────────────────────────────────────────────────────┐
│ get_event_loop / EventLoopPolicy │
│ 3.10 部分场景警告 → ★3.12 明确弃用★ → 3.16 计划移除 │
│ loop= 参数:3.8 弃用 → ★3.10 移除★ │
│ @asyncio.coroutine:3.8 弃用 → ★3.11 移除★ │
│ asyncio.async():早已移除(改名 ensure_future) │
└────────────────────────────────────────────────────────┘
★ 趋势总结(★这是理解所有变化的钥匙★):
① ★从"手动管理 loop"到"asyncio.run / Runner 托管"★
② ★从"松散的并发原语"到"结构化并发(TaskGroup)"★
③ ★从"隐式全局状态(policy、get_event_loop)"到"显式传递"★
④ 持续的性能优化(3.11 尤其明显)
3.12 的核心主题是清理事件循环的管理方式:弃用 get_event_loop() 和整个 EventLoopPolicy 体系(3.16 计划移除,连带 uvloop.install() 也被弃用),改用 asyncio.run(main(), loop_factory=...) 或 asyncio.Runner(后者能复用同一个 loop,解决「asyncio.run 每次新建销毁 loop 导致连接池资源全丢」的问题)。3.12 还引入了 eager_task_factory(任务创建时立即同步执行到第一个 await,对「大部分任务能同步完成」的场景能省下调度开销)。3.13 加入了 Queue.shutdown()——终于有了优雅关闭队列的标准方式(以前只能靠哨兵值)。理解所有这些变化的钥匙是四条趋势:从「手动管理 loop」到「asyncio.run/Runner 托管」、从「松散的并发原语」到「结构化并发」、从「隐式全局状态」到「显式传递」、以及持续的性能优化。
五、老代码迁移清单
★ 按优先级排列的迁移项:
★P0(会直接报错)★
□ ★loop= 参数★(3.10 起 TypeError)
asyncio.Queue(loop=loop) → asyncio.Queue()
□ ★@asyncio.coroutine + yield from★(3.11 起移除)
@asyncio.coroutine
def f(): yield from g() → async def f(): await g()
□ Task.current_task() / Task.all_tasks()(早已移除)
→ asyncio.current_task() / asyncio.all_tasks()
□ asyncio.async()(关键字冲突,早已改名)→ ensure_future
★P1(行为静默改变,★最危险★)★
□ ★except Exception 捕获 CancelledError★(3.8 起抓不到了)
→ 显式 except asyncio.CancelledError 并 ★raise★
□ 依赖 get_event_loop() 在非协程环境创建 loop 的行为
→ 3.12+ 会警告,未来会报错
★P2(弃用警告,未来会移除)★
□ get_event_loop() → ★协程内用 get_running_loop()★
□ set_event_loop_policy / uvloop.install()
→ ★asyncio.run(main(), loop_factory=...)★
□ 自定义 event_loop fixture(pytest-asyncio)
→ asyncio_default_fixture_loop_scope / loop_scope 标记
★P3(可以改进但不紧急)★
□ gather → ★TaskGroup★(3.11+,语义更安全)
□ wait_for → ★asyncio.timeout()★(3.11+,能包多条语句)
□ run_in_executor → ★to_thread★(3.9+,会传 contextvars)
□ 哨兵值关闭队列 → ★Queue.shutdown()★(3.13+)
□ ensure_future → create_task(语义更明确)
★ 检测手段:
① ★python -W error::DeprecationWarning app.py★
→ 把弃用警告变成错误,一次性暴露所有问题
② python -X dev app.py(开发模式,包含更多检查)
3 CI 里加 filterwarnings = error
④ ruff / pyupgrade:★pyupgrade --py311-plus 能自动改写一部分★
⑤ 搜索关键字:get_event_loop / ensure_future / loop= / @asyncio.coroutine
★ 写"兼容多版本"代码的建议:
① ★优先用高层 API★(asyncio.run、create_task、to_thread)
→ 它们的语义最稳定,低层 loop API 变化最频繁
② 需要新特性又要兼容老版本:
if sys.version_info >= (3, 11):
async with asyncio.timeout(5): ...
else:
await asyncio.wait_for(..., timeout=5)
③ 库作者:★用 anyio★ 可以同时兼容不同版本和 trio
④ ★明确声明最低支持版本★(setup.py 的 python_requires)
→ 与其到处写兼容分支,不如提高门槛
迁移按优先级分四档。P0(会直接报错):loop= 参数、@asyncio.coroutine + yield from、Task.current_task()。P1(行为静默改变,最危险):用 except Exception 捕获 CancelledError(3.8 起抓不到了,导致清理逻辑不再执行)、依赖 get_event_loop() 在非协程环境创建 loop 的行为。P2(弃用警告):get_event_loop、set_event_loop_policy/uvloop.install()、pytest-asyncio 的自定义 event_loop fixture。P3(可改进不紧急):gather → TaskGroup、wait_for → asyncio.timeout()、run_in_executor → to_thread。检测手段里最有效的是 python -W error::DeprecationWarning(把弃用警告变成错误,一次性暴露所有问题)和 pyupgrade --py311-plus(能自动改写一部分)。写兼容代码的建议是:优先用高层 API(它们语义最稳定,低层 loop API 变化最频繁)、必要时用 sys.version_info 分支、库作者可以用 anyio、以及明确声明最低支持版本(与其到处写兼容分支,不如提高门槛)。
六、写”面向未来”的异步代码
★ 六条经得起版本变迁的实践:
① ★永远用 asyncio.run() 作为入口★
asyncio.run(main())
→ 3.7 以来最稳定的 API;它会正确处理清理
→ 需要复用 loop 时用 asyncio.Runner(3.11+)
② ★协程内一律用 get_running_loop()★
→ 语义明确,不会创建意外的 loop
→ 判断"是否在异步上下文"的标准写法
③ ★用高层 API,避开 loop 的低层方法★
asyncio.create_task > loop.create_task
asyncio.to_thread > loop.run_in_executor
asyncio.timeout > loop.call_later + 手动取消
→ ★低层 API 是变化最频繁的部分★
④ ★3.11+ 用 TaskGroup 和 asyncio.timeout★
它们代表了 asyncio 的发展方向(结构化并发)
→ 写出来的代码更安全,也更符合未来的最佳实践
⑤ ★CancelledError 单独处理并重新抛出★
try: ...
except asyncio.CancelledError:
await cleanup(); raise
except Exception: ...
→ 这个模式在所有版本都正确
⑥ ★CI 里开 -W error::DeprecationWarning★
→ 新版本引入的弃用会★立刻暴露★,而不是等到升级时爆炸
★ 版本选择建议(2026 年):
┌──────────┬────────────────────────────────────────┐
│ 3.9 及以下│ ★强烈建议升级★(缺 TaskGroup、to_thread)│
│ 3.10 │ 可用,但缺 3.11 的结构化并发和性能优化 │
│ ★3.11★ │ ★推荐的最低版本★(TaskGroup + 大幅提速) │
│ 3.12/3.13│ ★推荐★(loop 管理清晰、Queue.shutdown) │
│ 3.14 │ 新项目可用(free-threading 官方支持) │
└──────────┴────────────────────────────────────────┘
★ 如果只能记一条:★3.11 是 asyncio 的重要分水岭★
★ 面试怎么答(★要点式★):
"asyncio 演进很快,几个关键节点:
★3.7 有了 asyncio.run 和 create_task★,不用再手动管理事件循环;
★3.8 CancelledError 改为继承 BaseException★,except Exception 抓不到取消了;
3.9 加了 to_thread(比 run_in_executor 简洁且传 contextvars);
★3.10 移除了所有 loop= 参数★;
★3.11 是第二个大版本:TaskGroup 结构化并发、asyncio.timeout、
ExceptionGroup,还有零成本异常带来的大幅提速★;
★3.12 起 get_event_loop 和 EventLoopPolicy 进入弃用流程★,
改用 asyncio.run(loop_factory=) 或 Runner。
整体趋势是:从手动管 loop 到框架托管、从松散并发到结构化并发、
从隐式全局状态到显式传递。"
六条经得起版本变迁的实践:永远用 asyncio.run() 作为入口(3.7 以来最稳定的 API,需要复用 loop 时用 Runner)、协程内一律用 get_running_loop()、用高层 API 避开 loop 的低层方法(低层 API 是变化最频繁的部分)、3.11+ 用 TaskGroup 和 asyncio.timeout(代表 asyncio 的发展方向)、CancelledError 单独处理并重新抛出(这个模式在所有版本都正确)、CI 里开 -W error::DeprecationWarning(让新版本引入的弃用立刻暴露而不是升级时爆炸)。版本选择上,3.11 是推荐的最低版本(有 TaskGroup 和大幅性能提升),3.12/3.13 更佳。如果只能记一条:3.11 是 asyncio 的重要分水岭。
记忆钩子:「asyncio 是标准库里演进最快的模块,★网上大量教程停留在 3.5~3.6 的写法★。关键节点:★3.7 是第一个分水岭★——有了 ★asyncio.run()★(不用再 get_event_loop + run_until_complete,而且它会正确地取消剩余任务、关闭异步生成器和默认 executor)、★create_task★(替代语义模糊的 ensure_future)、★get_running_loop★(替代语义混乱的 get_event_loop,没有运行 loop 就直接报错)、contextvars。★3.8 最重要:CancelledError 从 Exception 改为继承 BaseException★——except Exception ★再也抓不到取消★(★有意设计★,防止业务代码误吞取消信号导致任务『拒绝取消』),同期还有 AsyncMock、Windows 默认 Proactor。★3.9 加了 to_thread★(不只是简写,★关键是它会传递 contextvars 而 run_in_executor 不会★)。★3.10 移除了所有 loop= 参数★(Queue(loop=)、sleep(1, loop=) 全部 TypeError)。★3.11 是第二个大版本★:★TaskGroup(结构化并发:一个失败取消其余 + 退出时保证无遗留 + 自持强引用 + ExceptionGroup)★、★asyncio.timeout()(上下文管理器形式,可包多条语句、能 reschedule)★、except*,外加★零成本异常处理带来的大幅提速★(不用改代码);顺带 ★asyncio.TimeoutError 成为内置 TimeoutError 的别名★。★3.12 起 get_event_loop 和整个 EventLoopPolicy 进入弃用流程(3.16 计划移除)★,连带 uvloop.install() 也弃用 → 改用 ★asyncio.run(main(), loop_factory=)★ 或 ★asyncio.Runner(可复用 loop)★;还有 eager_task_factory。3.13 加了 ★Queue.shutdown()★(以前只能用哨兵值)。★迁移时最危险的是 P1 类『行为静默改变』——用 except Exception 捕获 CancelledError★;检测手段是 ★python -W error::DeprecationWarning★。趋势总结:★从手动管 loop 到框架托管、从松散并发到结构化并发、从隐式全局状态到显式传递★。」
七、常见误区与追问
- 误区:网上教程写的
loop = asyncio.get_event_loop(); loop.run_until_complete(main())是标准写法。 这是 Python 3.4~3.6 时代的写法,现在既不推荐也不安全。3.7 起应该用asyncio.run(main())——它不仅更简洁,做的事还比手写版本完整得多:运行主协程之后会取消所有剩余任务并等它们收尾、关闭异步生成器(shutdown_asyncgens)、关闭默认线程池(3.9+)、最后才关闭 loop。手写的run_until_complete + close几乎总会漏掉中间几步,结果就是退出时满屏Task was destroyed but it is pending!、异步生成器的清理代码没执行、线程池里的任务被丢弃。而且get_event_loop()从 3.12 起已进入弃用流程(3.16 计划移除),继续用只会在升级时爆炸。 - 误区:
except Exception能捕获所有异常,包括任务被取消。 从 Python 3.8 起不行了——asyncio.CancelledError被改为继承BaseException而不是Exception。这是有意的设计:取消是一种「控制流信号」而不是「错误」,如果被业务代码里宽泛的except Exception吞掉且不重新抛出,任务就变成了「不可取消」——task.cancel()调用了却停不下来,优雅关闭直接超时,最后被SIGKILL强杀。危险之处在于这是行为的静默改变:从 3.7 迁移过来的代码不会报错,只是「原来能在取消时执行的清理逻辑,现在不执行了」。正确模式在所有版本都成立:先except asyncio.CancelledError: await cleanup(); raise(必须重新抛出),再except Exception:处理业务异常。 - 误区:
asyncio.Queue(loop=loop)这种显式传 loop 的写法更严谨。 3.10 起所有 asyncio API 的loop=参数都被移除了,这么写会直接抛TypeError: got an unexpected keyword argument 'loop'(受影响的包括Queue、Lock、Event、Semaphore、Condition、sleep、gather、wait等)。移除的原因恰恰是「显式传 loop 更容易出错」:这些同步原语和 Future 本来就应该绑定到「运行时的那个 loop」,允许显式传递只会制造跨 loop 使用的 bug(一个在 loop A 上创建的Lock被 loop B 上的协程 await,会抛got Future attached to a different loop)。迁移方法很简单:把loop=参数直接删掉,并确保这些对象在协程内部创建(这样它们自然绑定到当前运行的 loop)。 - 误区:
asyncio.to_thread()只是loop.run_in_executor(None, fn)的语法糖。 简洁只是表象,关键区别是to_thread会传递contextvars——它内部执行ctx = contextvars.copy_context()然后把ctx.run(fn, *args)交给线程池,所以线程里的同步代码能读到request_id、trace_id这些上下文变量;而直接run_in_executor(None, fn)不做任何上下文传递,函数在工作线程的默认上下文里运行,日志里的 trace_id 会丢失。此外to_thread还支持关键字参数(run_in_executor只能传位置参数,要用functools.partial包装)。所以 3.9+ 应该默认用asyncio.to_thread,只有需要指定自定义线程池时才用run_in_executor(pool, fn)(此时如果需要上下文,要自己copy_context())。 - 误区:
gather和TaskGroup只是新旧写法的区别,不着急迁移。 差别是语义级的,而且都是安全相关的。gather在某个任务抛异常时立即把异常抛给调用方,但不取消其余任务——它们继续在后台运行、继续占资源、它们的异常也无人认领;而且gather返回后可能还有任务在跑(逃逸到了调用范围之外)。TaskGroup(3.11+)保证四件事:一个任务失败会取消所有兄弟任务、退出async with块时所有任务必然已结束、自己持有强引用(不用担心任务被 GC)、所有异常汇总成ExceptionGroup(而不是只看到第一个)。所以在需要「一荣俱荣、一损俱损」语义的地方(绝大多数并发场景),TaskGroup能消除一整类 bug。只有「部分失败可接受、希望所有任务都跑完」的批量场景才该继续用gather(return_exceptions=True)。 - 追问:为什么
get_event_loop()要被弃用?它的问题在哪? 它的语义随调用环境而变,且历史上变过多次,是 asyncio 里最令人困惑的 API:在协程内部调用返回当前运行的 loop;在主线程且没有运行 loop 时,早期版本会自动创建一个新的 loop 并设为当前 loop(这导致「明明没跑异步代码,却凭空多了一个 loop」,还容易与asyncio.run创建的 loop 冲突);在非主线程且没有设置过 loop 时则直接报错。这种「有时创建、有时返回、有时报错」的行为让人无法预测,也是很多「跨 loop 使用对象」bug 的源头。所以 3.7 引入了语义明确的get_running_loop()(没有运行中的 loop 就直接RuntimeError),3.10 开始对危险用法告警,3.12 正式弃用get_event_loop和整个EventLoopPolicy体系(后者是全局单例,与asyncio.run的显式生命周期管理模型冲突),3.16 计划移除。现在的正确做法是:协程内用get_running_loop(),需要自定义 loop 用asyncio.run(main(), loop_factory=...)或asyncio.Runner。 - 追问:
asyncio.timeout()相比wait_for()好在哪? 三点。① 能包住多条语句:wait_for(coro, timeout)只能给一个协程设超时,而async with asyncio.timeout(5):可以让整段代码共享一个超时预算(await step1(); await step2(); await step3()总共不超过 5 秒)——这更符合「一次请求的总耗时预算」这种真实需求,用wait_for得给每一步分配子预算,既麻烦又不准。② 可以动态调整:async with asyncio.timeout(5) as cm: cm.reschedule(loop.time() + 10),适合「收到服务端的 Retry-After 后延长等待」这类场景。③ 语义更清晰:它是标准的上下文管理器,超时抛TimeoutError(3.11 起asyncio.TimeoutError就是内置TimeoutError的别名),而wait_for内部实际上会取消被包装的协程,这个副作用容易被忽略。要兼容 3.10 及以下时仍需wait_for,可以用sys.version_info分支或anyio.fail_after()。 - 追问:项目该从哪个 Python 版本开始支持 asyncio 新写法? 从 3.11 开始是当前(2026 年)最实际的建议,理由有三:①
TaskGroup和asyncio.timeout()是质变——结构化并发能消除「任务逃逸、异常丢失、遗留任务」一整类 bug,这比任何微观优化都重要;② 3.11 的性能提升不用改代码(零成本异常处理 + PEP 659 专用化解释器,异步程序整体提速可观);③ 3.10 及以下缺失的东西太多(没有 TaskGroup 就要自己处理 gather 的语义缺陷,3.9 以下连to_thread都没有)。如果必须支持更老的版本,两个策略:用sys.version_info做特性分支(只在少数几处),或者用anyio(它在老版本上也提供 task group 和fail_after,还能同时兼容 trio)。无论选哪个版本,在 CI 里开-W error::DeprecationWarning都是必须的——这样新版本引入的弃用会立刻暴露,而不是等到某次大升级时集中爆炸。
八、加强记忆
asyncio 是标准库里演进最快的模块,网上大量教程停留在 3.5~3.6 的写法。关键节点:3.7 是第一个分水岭——有了 asyncio.run()(不用再 get_event_loop + run_until_complete,而且它会正确地取消剩余任务、关闭异步生成器和默认 executor)、create_task(替代语义模糊的 ensure_future)、get_running_loop()(替代语义混乱的 get_event_loop,没有运行 loop 就直接报错)、以及 contextvars。3.8 最重要的变化是 CancelledError 从 Exception 改为继承 BaseException——except Exception 再也抓不到取消(这是有意设计,防止业务代码误吞取消信号导致任务「拒绝取消」、优雅关闭超时),同期还有 AsyncMock 和 Windows 默认改为 Proactor。3.9 加了 to_thread(不只是简写,关键是它会传递 contextvars 而 run_in_executor 不会)。3.10 移除了所有 loop= 参数(Queue(loop=)、sleep(1, loop=) 全部 TypeError)。3.11 是第二个大版本:TaskGroup(结构化并发——一个失败取消其余、退出时保证无遗留、自持强引用、异常汇总成 ExceptionGroup)、asyncio.timeout()(上下文管理器形式,可包多条语句、能 reschedule)、except*,外加零成本异常处理带来的大幅提速(不用改一行代码);顺带 asyncio.TimeoutError 成为内置 TimeoutError 的别名。3.12 起 get_event_loop 和整个 EventLoopPolicy 进入弃用流程(3.16 计划移除),连带 uvloop.install() 也被弃用 → 改用 asyncio.run(main(), loop_factory=...) 或 asyncio.Runner(可复用 loop);还引入了 eager_task_factory。3.13 加了 Queue.shutdown()(以前只能用哨兵值)。迁移时最危险的是 P1 类「行为静默改变」——用 except Exception 捕获 CancelledError;检测手段是 python -W error::DeprecationWarning(把弃用变成错误一次性暴露)。趋势总结:从手动管 loop 到框架托管、从松散并发到结构化并发、从隐式全局状态到显式传递、以及持续的性能优化——3.11 是当前推荐的最低版本。