Codex 源码解析 · 第 20 讲
Codex 源码解析系列 第 20 讲:Parallel 工具并行执行与编排 基于 OpenAI Codex 源码 · 2026-10-14 💡 本讲一句话: Codex 用一把 RwLock 当"并发门控"——只读工具拿读锁一起跑,写工具拿写锁排队;再用 FuturesOrdered 保证结果按模型发出顺序回来。读完你能看懂 parallel.rs / orchestrator.rs / executed_tool_calls.rs 三个文件如何拼出"并行但不乱序、可中止、可计时"的工具执行层。 一、为什么需要"并行 + 保序" 模型一次响应里可以同时发出多个工具调用:查两个文件、读一个 MCP 资源、搜一下插件列表——彼此毫无依赖。如果串行执行,总耗时就是所有调用之和;并行则接近最慢的那个。但"并行"立刻带来三个工程问题: 🔹 写冲突 :两个工具同时改同一个文件,结果不可预测——必须串行; 🔹 乱序 :任务完成顺序是随机的,但模型看到的工具输出顺序必须和它发出调用的顺序一致,否则下一轮推理会错乱; 🔹 中止竞态 :用户按 Esc 的那一刻,有的任务可能刚好跑完——不能把"其实成功了"误报成"被中止"。 Codex 的答案是三个机制的组合: RwLock 门控 (parallel.rs)、 FuturesOrdered 保序收集 (turn.rs)、 按工具声明的并发能力 (registry.rs + 各 handler)。下面逐个拆。 二、ToolCallRuntime:每个 step 一把并发门控锁 先看承载一切的结构体(codex-rs/core/src/tools/parallel.rs): 📄 codex-rs/core/src/tools/parallel.rs (第 43-63 行) #[derive(Clone)] // 派生 Clone:Arc 字段都是引用计数,克隆廉价——每个工具调用任务都要持有一份 pub(crate) struct ToolCallRuntime { // 工具调用的运行时上下文,绑定到某一个 step(模型的一次响应) session: Arc<Session>, // 会话句柄:审批、遥测、历史写入都挂在它上面 // Tool calls may run later, so retain the step whose tool list advertised them. // 注释原文:工具调用可能稍后才执行,所以必须保留"宣布过这批工具的那个 step"——防止 step 被回收后悬空引用 step_context: Arc<StepContext>, // step 上下文:持有 tool_router、turn、settings,是分发时的唯一事实来源 tracker: SharedTurnDiffTracker, // turn 级 diff 追踪器:多个并行工具共享同一份文件变更账本 parallel_execution: Arc<RwLock<()>>, // ★并发门控锁:锁住的是空值,纯粹当"读=可并行 / 写=独占"的信号量用 } impl ToolCallRuntime { // 实现块开始 pub(crate) fn new( // 构造函数 session: Arc<Session>, // 会话 step_context: Arc<StepContext>, // step 上下文 tracker: SharedTurnDiffTracker, // diff 追踪器 ) -> Self { // 返回自身 Self { // 字面量构造 session, // 原样放入 step_context, // 原样放入 tracker, // 原样放入 parallel_execution: Arc::new(RwLock::new(())), // ★每个 step 新建一把读写锁——并发域被限定在"同一次模型响应内",跨 turn 互不干扰 } // 构造结束 } // new 结束 为什么这样设计: 锁的粒度是 step (一次模型响应),不是全局。同一批工具调用共享一把 RwLock,天然表达"这批里只读可以并发、写必须独占";而不同 turn 的工具互不阻…
阅读全文:Codex 源码解析 第 20 讲