会 await 不等于会并发:先画依赖,再写 gather¶
学习助手生成计划要做四件事:读取用户偏好、读取课程目录、用两份数据生成计划、
保存计划。把四个调用一起放进 gather 看起来最“异步”,却会让生成操作拿不到
输入,让保存发生在计划产生之前。并发原语能调度工作,但不能替我们理解业务。
运行环境¶
Python 3.12、asyncio 标准库。示例用可控延迟模拟独立 I/O。
1. 用数据流决定并发层¶
先不写代码,只画箭头:
两个读取所需输入一开始就具备,互不使用对方结果,可以位于同一层。build_plan
需要两份结果,save_plan 又需要生成结果,所以后两步必须顺序执行。这与 ETL DAG
很像:同层节点可能并行,箭头连接的节点不能颠倒。
数据参数独立还不够。两个调用若同时写同一状态、共享非线程安全资源,或业务要求 “先持久化再通知”,仍可能必须顺序。信息不足时,正确答案是“先确认副作用”, 而不是猜测可以并发。
2. 把同层 I/O 组合,跨层依赖保留¶
import asyncio
async def load(name: str, delay: float) -> str:
await asyncio.sleep(delay)
return f"{name}:ok"
async def build_study_plan() -> dict[str, str]:
profile, catalog = await asyncio.gather(
load("profile", 0.12),
load("catalog", 0.04),
)
plan = f"plan({profile}, {catalog})" # 短本地计算
saved = await load(f"save:{plan}", 0.02) # 依赖 plan
return {"profile": profile, "catalog": catalog, "saved": saved}
gather 并发调度两个 awaitable,等全部完成后返回。这里使用它,是因为依赖分析
已经证明两个读取属于同层,而不是因为函数写着 async。生成计划是短本地计算,
无需伪装成 coroutine;保存依赖生成结果,必须位于下一层。
本课只使用 gather 的正常成功路径。一个子任务失败后兄弟任务如何处置、是否允许
部分结果,是后续异常与取消课程的内容,不能从这个示例擅自推导生产策略。
3. 完成顺序不能决定业务身份¶
上例中 catalog 先完成,profile 后完成,但 gather(profile_call, catalog_call) 的
返回顺序与传入 awaitable 的顺序一致,不按完成快慢重新排列。因此慢的 profile
仍解包到 profile。
当调用数量动态变化时,位置可能不够直观,应保存显式身份:
calls = {
"profile": load("profile", 0.12),
"catalog": load("catalog", 0.04),
}
values = await asyncio.gather(*calls.values())
results = dict(zip(calls.keys(), values, strict=True))
核心原则不是必须用字典,而是业务身份不能依赖不可预测的完成顺序。否则某次网络 抖动就可能把“用户偏好”当成“课程目录”。
4. 并发块也需要所有者¶
当前 service operation 创建的并发工作应在函数返回前完成。审查时问四件事:
- 谁创建这些工作?
- 谁等待它们?
- 什么条件表示这一组工作完成?
- 每个结果怎样回到正确业务身份?
若代码 create_task 后直接 return,依赖图即使画对了,任务仍逃逸了。后台托管是 另一种产品语义,需要独立设计,不能由“忘了 await”偶然得到。
边界与常见误区¶
- 所有调用都是 async,不代表它们相互独立。
- 连续 await 保持顺序;是否应改为并发由依赖决定,而不是耗时决定。
- 可并发不等于必须并发;简单、低流量路径可能选择顺序执行。
- 不按完成先后猜结果身份。
gather不是依赖分析器,也不是本课的异常处理方案。
本章小结¶
正确顺序是:先画数据与副作用依赖,再找出可重叠的同层 I/O,最后选择执行原语。 依赖调用保持顺序,结果保持稳定身份,所有任务留在明确父作用域中。API 只是把 设计落地,不能替代设计。
练习¶
- “读取用户”“读取用户订单”“根据订单推荐课程”中哪些可以并发?还缺什么信息?
- 修正:
plan, profile = await gather(load_profile(), build_plan())。 - 两个调用延迟每次变化,怎样保证结果不会错配?
练习解析¶
- 若读取订单需要用户 ID,则必须先读取用户;推荐又依赖订单。若 ID 已由请求直接 提供,前两个读取是否可并发还要检查共享资源和副作用。
- 先取得构建计划所需的全部输入;只有独立读取进入 gather,
build_plan在其后 执行。不能靠调换变量名修复不存在的输入。 - 使用
gather的声明顺序、显式键或任务到业务 ID 的映射;不要用完成顺序命名。