雨天小六

读懂 Codex(3.18):CancellationToken 与 Tokio Task 取消的协作边界

· 更新于 2026-08-02 · 专栏:读懂 Codex

#Codex#Agent Runtime#生命周期#软件架构

CancellationToken 是协作通知,Tokio JoinHandle::abort 是强制停止 future。Codex 同时使用两者:先给内部组件机会生成一致的 aborted output、关闭进程和释放 waiter;100ms 内主 Task 未结束,再强制 abort 保证 Interrupt 有上界。

取消树怎样形成

RunningTask 持有根 token。RegularTask 收到 child,run_turn 再为 sampling、Step capture 和工具调用创建 child。父 token 取消会传播给所有后代;取消某个 child 不反向取消父节点,也不会影响兄弟调用。

RunningTask 根 CancellationToken 向 run_turn、sampling 和工具调用派生子 token
图 3.18-1:树形 token 把“一轮全部停止”和“单个子操作停止”分开;共享一个平面 bool 无法表达这种方向性。
task_token = CancellationToken()
regular_token = task_token.child_token()
sampling_token = regular_token.child_token()
tool_a_token = sampling_token.child_token()
tool_b_token = sampling_token.child_token()

task_token.cancel()     # A/B/stream 全部收到
tool_a_token.cancel()   # 只影响 A

异步函数必须主动在 await 边界检查 token,例如 select(operation, token.cancelled()).or_cancel(token)。纯 CPU 循环若从不 yield,token 不能抢占它;这正是外层保留 handle.abort 的原因。

done Notify 与 JoinHandle 各自证明什么

wrapper 无论 task result如何都会 done.notify_waiters()。Abort 路径先 cancel,然后 select(done.notified(), sleep(100ms))。done 表示业务 wrapper 已运行到末尾;JoinHandle 则代表 Tokio 调度实体。宽限结束后调用 handle.abort,即使 done 未到也能阻止 future 继续推进。

取消先等待 100ms 协作退出,再强制 abort Tokio task
图 3.18-2:100ms 是主 Task 的协作窗口,不是所有外部进程一定已退出的证明;需要运行时收尾的工具会在自己的取消协议中等待。
async def abort_running_task(task):
    if task.token.is_cancelled():
        return
    task.token.cancel()
    await cancel_git_enrichment()

    completed = await select(
        task.done.notified(),
        timeout(milliseconds=100),
    )
    if not completed:
        warn("task did not stop gracefully")
    task.handle.abort()
    await task.strategy.abort(session, task.turn)

AbortOnDropHandle 是最后一道所有权保险

RunningTask 用 AbortOnDropHandle 包装 JoinHandle,防止记录被意外 drop 后后台 future 继续运行。正常完成时 on_task_finished 先取出 RunningTask 并 detach(),因为此刻正在该 handle 所代表的任务内部完成自己;若 drop 仍触发 abort,会出现自我取消竞态。

因此取消正确性来自三层:token 协作、explicit abort 上界、AbortOnDrop 所有权兜底。仅有其中一层都不完整。

评论


← 返回文章列表