← 返回题目列表

asyncio 各版本有哪些重要变化?老代码怎么迁移?

中等 第 25 / 27 题 更新于 2026/08/01
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.8CancelledError 改成继承 BaseExceptionexcept Exception 从此抓不到取消,这是有意设计),并加入 AsyncMock3.9 加入 asyncio.to_thread()(比 run_in_executor 简洁且会传递 contextvars)。3.10 移除了所有 API 的 loop= 参数(老代码传 loop=loop 会直接报错)。3.11 是第二个大版本asyncio.TaskGroup(结构化并发)、asyncio.timeout()(上下文管理器形式的超时)、ExceptionGroupexcept*,外加大幅性能优化。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 CancelledErrorBaseException、3.10 移除 loop=、3.11 有了 TaskGroup/timeout、3.12+ 弃用 get_event_loop

详细版

版本速查表

版本关键变化
3.7asyncio.run()create_task()get_running_loop()contextvars
3.8CancelledError 继承 BaseExceptionAsyncMock、Windows 默认 Proactor
3.9asyncio.to_thread()loop.shutdown_default_executor()
3.10移除所有 loop= 参数get_event_loop 无运行 loop 时告警
3.11TaskGroupasyncio.timeout()ExceptionGroup/except*、性能大幅优化
3.12弃用 get_event_loop/EventLoopPolicyasyncio.Runnerrun(loop_factory=)
3.13Queue.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 from3.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+ 应该默认用它替代 gatherasyncio.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 fromTask.current_task()P1(行为静默改变,最危险)except Exception 捕获 CancelledError(3.8 起抓不到了,导致清理逻辑不再执行)、依赖 get_event_loop() 在非协程环境创建 loop 的行为。P2(弃用警告)get_event_loopset_event_loop_policy/uvloop.install()、pytest-asyncio 的自定义 event_loop fixture。P3(可改进不紧急)gather → TaskGroupwait_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+ 用 TaskGroupasyncio.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'(受影响的包括 QueueLockEventSemaphoreConditionsleepgatherwait 等)。移除的原因恰恰是「显式传 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_idtrace_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())。
  • 误区:gatherTaskGroup 只是新旧写法的区别,不着急迁移。 差别是语义级的,而且都是安全相关的。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 年)最实际的建议,理由有三:TaskGroupasyncio.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 就直接报错)、以及 contextvars3.8 最重要的变化是 CancelledErrorException 改为继承 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_factory3.13 加了 Queue.shutdown()(以前只能用哨兵值)。迁移时最危险的是 P1 类「行为静默改变」——用 except Exception 捕获 CancelledError;检测手段是 python -W error::DeprecationWarning(把弃用变成错误一次性暴露)。趋势总结:从手动管 loop 到框架托管、从松散并发到结构化并发、从隐式全局状态到显式传递、以及持续的性能优化——3.11 是当前推荐的最低版本