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 不反向取消父节点,也不会影响兄弟调用。
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 继续推进。
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 所有权兜底。仅有其中一层都不完整。
评论
登录后即可评论