看门狗线程:只能报告超时,不能跨线程修改VM
自动关联目录:看门狗线程:只能报告超时,不能跨线程修改VM
核心判断:后台看门狗可以测量截止时间并发布超时信号,但不能在脚本正由 Game Thread 执行时,从工作线程读取或替换 VM Hook。中断必须由 VM 所在线程在受支持的 safepoint 消费。
一、业务冲突:主线程已经卡住,普通 Timer 无法救场
创作者脚本出现死循环时,Game Thread 不会进入下一帧,基于 Tick 或 Timer 的超时检查也无法运行。工程因此创建低优先级监测线程,用帧开始时间判断本帧是否超限;脚本超时尝试安装 line hook,原生执行严重超时则请求退出进程。这条路线能在主线程失去调度时继续观测,作为最后熔断有现实价值。
二、实际实现:检测和中断耦合后,线程边界被穿透
看门狗在 Core BeginFrame 上记录时间,把检查任务放入 MPSC 队列,析构时 Stop、等待线程完成并移除帧委托。使用线程安全计数器表达部分状态,也是正确方向。
关键问题是超时处理直接在监测线程取得全局脚本状态,读取旧 Hook,再给主状态和当前协程安装新 Hook。此时 VM 很可能正在 Game Thread 内执行,脚本 C API 与共享 Hook Map 并没有一套覆盖全部访问的串行协议。
周边状态也暴露了同一问题:帧开始时间和当前协程是跨线程普通变量;Hook Map 有锁,但进入 Guard 时清空 Map 不持同一把锁;排队任务额外 new 一份对象并让 Lambda 负责删除,队列被直接清空时释放责任会消失;线程 Run 退出后指针仍非空,同一看门狗对象不能可靠重新启动。
这些细节不是五个独立 Bug,而是同一个根因:线程、任务、VM 和 Guard 没有统一状态机。
三、目标设计:监测线程发布信号,VM 线程执行中断
struct VmDeadline {
TAtomic<uint64> generation;
TAtomic<int64> deadlineCycles;
TAtomic<bool> timedOut;
};
// Watchdog thread: no VM API
void CheckDeadline(VmDeadline& d, uint64 observedGeneration) {
if (d.generation.Load() == observedGeneration && Cycles() >= d.deadlineCycles.Load())
d.timedOut.Store(true, EMemoryOrder::Release);
}
// VM-owning thread: installed once when the VM is created
void VmInterrupt(lua_State* L) {
auto& d = DeadlineFor(L);
if (d.timedOut.Exchange(false, EMemoryOrder::Acquire))
RaiseTimeoutOnThisThread(L);
}VM 创建时在所属线程一次性安装受支持的 interrupt/instruction hook;每次执行只更新 Generation 和 Deadline。看门狗持有纯 C++ 共享状态,不保存裸协程指针,不调用任何 VM API。退出 Guard 通过 Generation 让旧信号失效,Stop 采用 Stopping -> Join -> Stopped,委托解绑与任务释放各有唯一所有者。
四、能力边界:原生长调用无法靠脚本 Hook 强停
脚本停在同步 C++ 绑定内部时没有 VM safepoint,安全的 Hook 也无法及时运行。不能用另一个线程强行改 VM 或终止 Game Thread来伪装可中断。高成本原生调用必须在进入前扣预算、设置输入规模上限,或改造成可分片/异步操作;进程退出只适合不可恢复的最后止损,不是日常调度机制。
这与“脚本预算”是两层问题:预算决定每帧允许做多少;本篇只解决超时信号怎样安全送回 VM。
五、取舍与验证
safepoint 中断只能在 VM 到达检查点时生效,会增加极小的指令检查成本;换来的是不跨线程触碰 VM。测试覆盖纯脚本死循环、协程切换、pcall 吞错、Guard 快速重入、VM 销毁与超时同帧、Watchdog Stop、长原生调用和多 VM 并行。
观测 Deadline 到发现延迟、发现到 safepoint 延迟、错误 VM 命中数、过期 Generation 拒绝数、存活监测线程数和未释放任务数。最重要的不变量是:任何脚本 C API 都只在 VM 所属线程调用;看门狗唯一允许写入的是线程安全的超时信号。