浏览器 Worker AI 任务协议:取消、背压与代际隔离
把推理搬进 Worker 只能避免主线程卡顿,不能自动解决串结果、积压和取消。本文设计可解码消息 envelope、合作式停止、硬终止与 generation 隔离协议。
本文要点
- 主题聚焦:AI、TypeScript、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
把 embedding、图像预处理或端侧推理放进 Web Worker 能保持 UI 响应,却不保证可靠并发。旧任务可能覆盖新结果,大 ArrayBuffer 克隆可使内存翻倍,同步 Wasm 还会阻塞 cancel。Web Workers 并发基础 介绍线程与 postMessage,本篇处理任务协议和失败边界。
消息必须能独立验证和排序
双方使用带版本的 envelope。入口先把 event.data 当作 unknown 解码,检查类型、长度和枚举后再进入状态机;TypeScript 接口不能阻止错误的运行时数据。
type WorkerCommand = {
protocol: 3;
kind: "start" | "cancel" | "dispose";
taskId: string;
generation: number;
sequence: number;
payload?: unknown;
};
type WorkerEvent = {
protocol: 3;
kind: "accepted" | "progress" | "result" | "canceled" | "failed";
taskId: string;
generation: number;
sequence: number;
payload?: unknown;
};
taskId 标识一次逻辑工作,generation 每次重跑递增,sequence 在该代单调增加。主线程只应用当前 generation 且序号更大的事件;旧 Worker 即使在终止前吐出结果,也无法改写当前视图。协议版本不兼容时返回有限错误并重建 Worker,不能猜字段含义。
发布清单记录 Worker bundle hash、模型/tokenizer digest、协议与解码库版本、浏览器矩阵 digest。报告保存 Chromium、Firefox、WebKit 完整构建号和操作系统;功能支持用运行时检测,不能只看 User-Agent。
取消分成合作式与硬终止
cancel 适合能频繁让出事件循环的任务。Worker 在分块边界检查 canceled 标志,停止后释放 tensor、Wasm 内存和临时缓存,再发 canceled。不假设主线程 AbortSignal 在所有浏览器都可直接传递,协议只传稳定 ID 与原因。
同步长循环或不可中断 Wasm 不会处理排队的 cancel。超过取消期限时,主线程调用 worker.terminate(),丢弃该 Worker 的全部 generation,并新建实例。硬终止只能用于没有外部副作用或拥有幂等恢复的任务;浏览器内计算已消耗的 CPU/GPU 仍要计量。取消状态可以衔接 Agent 运行时状态机,但 Worker 的终止是进程资源边界。
AbortController 负责主线程 fetch、超时和组件卸载,并把原因映射为协议消息。端到端清理可参考 Next.js AI 流式响应治理,不能只停止 React 更新。
队列满时停止入队,继续排空
postMessage() 没有 desiredSize 或可等待的背压;命令序列化后会先进入浏览器的 port message task queue。Worker 收到消息后才统计等待数或返回 queue_full,无法限制此前已经排队的消息和 Transferable。因此应用队列上限不能冒充浏览器内部队列的硬上限。
Worker 启动时发放 task credit 与 byte credit。主线程调度器在调用 postMessage() 前,按任务数和 payload/Transferable 字节原子预留 credit;额度不足时只进入另一个有界上游队列或立即返回 queue_full。accepted 只把预留从“已发送”移到“Worker 等待/运行”,不返还额度;result、canceled、failed 或显式拒绝才返还。取消、发送异常和 Worker 崩溃也必须按 Worker generation 回收本地 credit,同时把执行结果保持为 unknown,不能把资源账本释放误写成业务未执行。
Worker 内部仍限定运行任务数、等待任务数和最老任务年龄,到高水位后继续处理已接受任务。交互任务可以 supersede 同一会话旧 generation;暂停 Worker 消费只会扩大积压。所有 start 必须经过同一个发送调度器,绕过 credit 直接调用 postMessage() 属于协议违规。
大块二进制优先通过 transfer list 转移所有权,发送后主线程必须视 buffer 为 detached。发送前先从 byte credit 预留 byteLength;若 postMessage() 抛错则立即返还,终态后再释放成功发送的额度。接收端再次检查 shape 与长度;Transferable 省去复制,不会自动保证内容可信。需要 SharedArrayBuffer 时另行满足 cross-origin isolation,并用 Atomics 定义生产/消费游标,不能把共享内存当普通数组。
Worker 崩溃、messageerror 或超时会使所有在途任务进入 unknown,上层按幂等性决定重试。不能因为没收到 result 就断言任务从未执行。AI 草稿可保存输入与检查点,GPU/Wasm 中间对象不能作为持久事实。
用竞态测试证明协议
自动化用例覆盖重复 start、乱序 progress、旧 generation 晚到、结果后 cancel、传输后误读 buffer、credit 耗尽、发送异常、Worker 抛错、同步死循环和页面卸载。用同一微任务突发提交远超额度的 start,断言实际调用 postMessage() 的 outstanding task/bytes 始终不超过 credit;故意让旧任务在新任务后完成,确认 UI 只采用新代。硬终止后检查 Worker 数、内存、credit 和事件监听器回落。
发布门可要求:跨代污染为零;取消 P95 在预算内;硬终止后无旧事件生效;应用发送侧 outstanding 始终不超过 credit;三个批准浏览器矩阵全部通过。浏览器内部 port queue 不可直接观测,门禁不能把它表述为已测得的硬上限。新协议先支持双解码、只发送旧格式,再切少量用户到 v3;稳定后才停止 v2 写入。回滚恢复旧 bundle 与协议发送版本,新页面仍保留 v3 decoder 到观察期结束,避免旧标签页消息使回滚页面崩溃。
Worker 可靠性的核心不是“后台线程跑起来”,而是每个命令能验证、每个事件能归代、每次取消有期限、每次过载都能拒绝。这样主线程才不会用一份流畅 UI 掩盖后台已经失序。
延伸阅读
Related