# M08 并发、调度、重试与取消

> 本章产出：有界并发、显式排队、公平性和可解释取消语义。目标不是“并发数越大越快”，而是在限制内稳定完成任务。

## 1. 当一个快Demo变成一千个任务

单个任务只需几秒，不代表一千个任务同时到来仍只需几秒。模型限流、数据库连接、CPU、网络和工具API都有各自容量。无限创建协程可以把等待压力从队列转移到内存，并不会创造容量。

先区分入队任务数、活跃任务数、正在调用模型的数量、正在执行工具的数量与等待人工的数量。等待审批的任务应该只占持久状态，不占模型并发额度。

## 2. Little定律给你数量级，不给你万能容量规划

在稳定系统、统计口径一致的条件下，平均系统内任务数L、到达率λ与平均停留时间W满足`L=λW`。例如每秒接收2项任务，平均从进入到离开为5秒，平均在系统内约10项。

这10项不是“需要10个线程”的结论，也不直接给出P95。若长期到达率超过处理能力，队列持续增长，不再满足稳态假设。若W包含人工等待，L会很大，但执行资源未必同样大。

对Agent，模型与工具多次交替使资源需求随阶段变化。一个run级大Semaphore易于实现，但可能在等待工具时霸占模型配额。更细粒度的模型并发、工具并发、租户预算有时更有效，需要测量复杂度是否值得。

## 3. 并行什么时候保持语义

同一订单的“查询物流”和“读取当前政策”在独立输入、无共享写入时可以并行。计算退款资格依赖它们的结果，不能提前执行。两个写操作若争用同一余额，不能仅因模型同时提出就并行。

把工具调用看成依赖图，只有前置依赖已满足的节点可以进入就绪队列。写冲突、审批和预算也是依赖，不只是数据依赖。并行工具返回后应按call_id关联，不能按完成顺序误配。

Python的`TaskGroup`提供结构化并发，子任务失败会影响同组任务；`asyncio`取消是协作式控制，不会自动停止已经在远程系统执行的请求。[S28](../../references/SOURCES.md#s28)

## 4. 排队策略是有代价的选择

三个任务在0时刻到达，运行时长为8、1、1。单Worker FIFO完成时刻为8、9、10，平均9；已知时长的短任务优先完成时刻为1、2、10，平均约4.33。这个例子展示队头阻塞，但实际系统通常不知道真实时长。

用估计时长做调度会有预测误差，长期短任务优先还会饿死长任务。可以引入等待老化、每租户轮转或加权公平策略，并报告长任务与小租户的尾延迟，不只报告全局均值。

本课程模拟器的运行时长是公开输入，因此其SJF结果是教学上界对照，不能当成真实Agent调度效果。

<!-- interaction:I16 -->

## 5. 并发限制、速率限制、预算限制不是一回事

并发限制约束同一时刻正在执行的请求数；速率限制约束一个时间窗口或令牌桶中的请求/token消耗；预算限制约束一项任务或租户累计能花多少资源。

一个请求可能需要先预留最大可能费用，再按实际usage结算释放余额。若多个Worker分别读“还剩100”后各花80，没有原子预留就会超预算。真实usage不可得时要明确估算策略和对账机制，不能用零填补缺失值。

模型输入token可以预估，输出token需要设置上限。工具费用、sandbox时间与重试费用也应计入预算。只限制LLM调用次数会放过一次巨大的工具输出或长时间子进程。

## 6. 重试风暴如何产生

假设三层调用各自最多尝试3次，最坏可能放大到27次底层尝试。每一层都自认为“只重试两次”，组合后却耗尽配额。

重试应集中在明确拥有恢复语义的一层，采用总截止时间和总尝试预算。退避与随机抖动避免大量任务在同一时刻再次冲击服务；但抖动不是权限错误、格式错误或业务否决的修复方法。

对于读工具的暂时错误，可以在预算内重试；对写工具结果未知，先核对同一operation，不生成新操作。对429等错误需遵守实际服务返回的限制信息，并防止SDK内部重试与应用层重试叠加。幂等语义仍是重试的前提。[S29](../../references/SOURCES.md#s29)

## 7. 取消应沿任务树传播

run取消后，停止创建新模型请求和工具任务；取消可取消的本地协程；关闭结果消费者；对sandbox子进程发终止信号，超过宽限期后强制停止；回收租约和临时资源。每一步都要区分“已请求”和“已确认”。

将阻塞函数放进线程并不能保证取消协程时线程停止。HTTP请求超时也不能保证服务器没执行。对子进程还要考虑其派生进程，不能只杀一个父PID就认为全部结束。

若取消到来时退款已提交，则run可能进入`RECONCILING`，待确认结果后给出准确状态。取消传播与业务结果核对是两个协作流程。

<!-- interaction:I17 -->

## 8. 过载时拒绝比排无限长队更诚实

系统可以限制排队长度、按租户配额拒绝新任务、给出可重试响应和预计状态。这里的“预计”不能假装精确。任务具有截止时间时，应避免执行已不可能有用的工作。

背压需要向上游传递：工具变慢使就绪队列增长，协调器减少发起新任务，API适当拒绝，而不是每层继续无限缓存。观察队列年龄比只看队列长度更能发现被长期忽视的任务。

## 9. 实验与岗位表达

L07先用确定性调度器比较FIFO与SJF，再为sandbox任务加入CPU/输出/时间约束。测量平均完成时间、最差等待时间、每租户分布和取消后资源回收。

面试回答不能停在“用了asyncio和Redis”。你应该说清楚：资源在哪里受限，为什么选择这个调度粒度，重试由哪层负责，旧Worker如何处理，取消在外部副作用上有什么边界。
