跳转至

最后一遍代码审查:每个等待点和 Task 都有归宿吗?

前四章分别回答了“何时用 async”“对象怎样执行”“哪些 I/O 能并发”和“阻塞调用 放哪里”。真实代码不会按章节分开犯错:一个 service operation 可能同时存在错误 并发、结果错配和 event loop 阻塞。本章用四层审查把这些判断接回一条数据流。

运行环境

Python 3.12、asyncio 标准库。示例仍使用可控 adapter,不代表主项目已实现。

1. 先看工作,不先数 async 关键字

目标操作包含:校验主题、读取用户偏好、读取课程目录、组合计划、调用遗留审计、 保存计划。第一遍只分类:

操作 分类 初步边界
校验与组合 短本地计算 普通函数直接完成
读取偏好、目录 独立 I/O 可考虑同层并发
保存计划 依赖 I/O 等计划生成后执行
遗留审计 阻塞 I/O 优先替换;暂时隔离

分类让后续问题有顺序:先证明为什么需要异步,再决定对象如何执行。反过来先把所有 函数改成 async,往往只会把边界藏得更深。

2. 核对 coroutine、Task 和所有权

import asyncio
import time

async def io(name: str, delay: float) -> str:
    await asyncio.sleep(delay)
    return f"{name}:ok"

def legacy_audit(topic: str) -> str:
    time.sleep(0.03)
    return f"audit:{topic}"

async def build_study_plan(topic: str) -> dict[str, str]:
    clean_topic = topic.strip()                 # 短本地计算
    profile, catalog = await asyncio.gather(    # 当前函数拥有并等待
        io("profile", 0.05),
        io("catalog", 0.03),
    )
    plan = f"{clean_topic}|{profile}|{catalog}"
    audit = await asyncio.to_thread(legacy_audit, clean_topic)
    saved = await io(f"save:{plan}", 0.01)     # 依赖 plan,保持顺序
    return {"plan": plan, "audit": audit, "saved": saved}

逐个回答:两个 coroutine 由 gather 调度并等待;调用方在组合完成前不会继续; 结果按声明位置关联;遗留调用由当前 Task 等待线程结果;保存依赖 plan;函数返回时 没有遗留 Task。这里不要求必须显式写 create_task,重要的是执行与完成条件明确。

若看到 asyncio.create_task(io(...)) 后直接 return,应立即追问谁等待和接收异常。 当前操作没有后台托管契约,所以最小修复是把任务纳入当前等待边界。

3. 核对依赖与阻塞,而不仅是总耗时

一段代码即使更快,也可能错误。把保存放进与读取同一个 gather,会破坏数据依赖; 按完成快慢给结果命名,会造成身份错配;在 coroutine 中直接调用 legacy_audit, 会让无关 Task 停顿。审查的三条证据是:

  • 只有输入已具备且副作用允许重叠的 I/O 被并发;
  • 有依赖的生成与保存保持顺序,结果身份不受完成时序影响;
  • 已知阻塞 I/O 不直接占用 event loop,并注明隔离后的限制。

这些证据比“本机跑起来很快”更稳定。耗时只能辅助观察,不能证明依赖正确。

4. 四层清单如何迁移

以后新增“读取推荐资料”时,不要直接把它塞进 gather。按同一顺序判断:

  1. 工作分类:它在等待 I/O、做短计算,还是持续占用 CPU?
  2. 执行与所有权:coroutine 怎样被驱动?若创建 Task,谁等待、何时完成?
  3. 依赖与关联:输入是否已具备?副作用能否重叠?结果如何保持身份?
  4. 阻塞边界:底层是否真正可 await?若隔离,剩余容量和取消限制是什么?

如果推荐资料只依赖 topic,且只读、接口原生 async,它可能与偏好和目录同层。 如果必须先知道用户等级,就应等待 profile。清单给出推理顺序,不给出脱离事实的 固定答案。

边界与常见误区

  • 能解释概念,不等于已经核对具体调用链。
  • 运行成功不证明没有 Task 逃逸、依赖颠倒或阻塞点。
  • 本课没有解决取消、timeout、异常收敛、并发上限和流式背压;应明确列为后续项。
  • 教材示例是边界说明,不表示 P1 主作品已经实现或通过测试。

本章小结

完整 async 边界审查不是搜索 asyncawait,而是依次核对工作分类、执行与 所有权、依赖与结果关联、阻塞边界。最终目标是让每个等待点都有理由、每个 Task 都有归宿、只有独立 I/O 被并发、依赖顺序不被破坏,阻塞调用不冻结 event loop。

练习

  1. 找错:并发读取 profile 与依赖 profile ID 的 orders。
  2. 找错:创建三个 Task 后只等待最快完成的一个便返回。
  3. 新增同步 PDF 解析调用,应放在哪里?还需确认哪些事实?
  4. 无资料写出四层审查清单,并应用于“读取推荐资料”。

练习解析

  1. orders 的输入尚未具备,必须先取得 profile ID;除非请求本身已提供独立 ID,且 还需确认副作用与资源约束。
  2. 另两个 Task 没有完成收敛和结果接收,所有权不完整。当前课程应等待整组工作; “只取最快结果”需要另行定义兄弟任务处置语义。
  3. 先判断它是阻塞 I/O 还是 CPU 密集解析、持续时间和库能力。阻塞 I/O 优先换 async 接口,无法替换时可隔离;CPU 密集则需评估其他模型。
  4. 顺序为工作分类、执行与所有权、依赖与关联、阻塞边界。推荐资料是否能进入 当前并发层,取决于输入、只读副作用和底层接口,而不是函数名。

参考资料