跳转到内容

Mutex FIFO 顺序执行模型

Mutex FIFO 顺序执行模型

学习目标

本章分析 Chrome DevTools MCP 的全局 Mutex 实现。你将理解:

  • 为什么浏览器操作需要顺序执行
  • Mutex 类的 FIFO 实现原理(Promise.withResolvers
  • Guard 模式的资源管理
  • 顺序执行的代价:无法并行操作不同标签页

项目实践

为什么需要 Mutex?

浏览器状态(页面列表、DOM、对话框、网络请求收集器)不是线程安全的。如果两个工具同时执行:

Agent A: click { uid: "1" } → 获取页面 → 点击元素
Agent B: navigate { url: "..." } → 导航到 URL

两个操作交叉执行可能导致:

  • Agent A 点击的页面在 Agent B 导航后被销毁
  • 网络请求收集器记录了混合的请求
  • 对话框状态被覆盖

解决方案:全局 Mutex,一次只允许一个工具执行。

Mutex 实现:FIFO 队列

class Mutex {
#locked = false;
#acquirers: Array<() => void> = [];
async acquire(): Promise<Guard> {
if (!#locked) {
#locked = true;
return new Guard(this); // 立即获得锁
}
// FIFO:排队等待
const {resolve, promise} = Promise.withResolvers<void>();
#acquirers.push(resolve);
await promise; // 等待前一个释放
return new Guard(this);
}
release(): void {
const resolve = #acquirers.shift();
if (!resolve) {
#locked = false; // 队列为空,完全释放
return;
}
resolve(); // 唤醒下一个等待者
}
}

关键特性

  • FIFO 顺序:先请求的先执行(#acquirers.shift()
  • Promise.withResolvers():现代 JavaScript 特性,创建一个外部的 resolve/reject 函数
  • 非自旋锁:等待者通过 await promise 挂起,不消耗 CPU

Guard 模式

const guard = await this.toolMutex.acquire();
try {
// 执行工具...
} finally {
guard.dispose(); // 释放锁
}

Guard 是一个内部类,dispose() 方法调用 Mutex.release()。这种模式的优势:

  • 明确的释放语义:调用者必须调用 dispose(),而不是依赖隐式行为
  • finally 配合:即使工具抛出异常,锁也会被释放

在 ToolHandler 中的应用

async handle(params): Promise<CallToolResult> {
const guard = await this.toolMutex.acquire();
const startTime = Date.now();
let success = false;
try {
// ... 工具执行 ...
success = true;
return result;
} catch (err) {
return { content: [{ type: "text", text: errorText }], isError: true };
} finally {
// 遥测记录
ClearcutLogger.get()?.logToolInvocation({
toolName: this.tool.name,
success,
latencyMs: bucketizeLatency(Date.now() - startTime),
});
guard.dispose(); // 无论成功/失败/异常,都释放锁
}
}

代价与局限

场景问题原因
两个 Agent 操作不同标签页必须排队,不能并行全局 Mutex,不区分页面
长时间操作(如 Lighthouse 审计)阻塞所有其他工具Lighthouse 需要 10-30 秒
高并发场景延迟递增FIFO 队列,每个工具等待前面的完成

潜在改进:可以设计按页面分片的 Mutex(每页一个 Mutex),但当前项目选择简单性优先。

设计取舍

取舍选择原因
全局 Mutex vs 每页 Mutex全局实现简单,避免跨页操作的竞态条件
FIFO vs 优先级队列FIFO公平性,避免优先级反转
同步锁 vs 异步锁异步Node.js 单线程,同步锁会阻塞事件循环

陷阱与对策

陷阱后果对策
finally 块中忘记 guard.dispose()死锁,后续工具永远阻塞handle() 方法强制在 finally 中调用
长时间操作不释放锁队列积压Agent 侧使用 pageIdRouting 分离长任务
Guard 对象被 GC 但未释放锁状态不一致Guard 是显式释放模式,不依赖 GC

参考来源