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 |