
1. AMCL 偶发 hang 一整晚
AMCL 持 map 锁调 localization,logging 回调又要 map 锁——经典 AB-BA。Nav2 costmap update 与 localization 若逆序加锁,偶发 hang,均值延迟正常、p99 尖峰。mutex 无 discipline 就是随机死锁生成器;规定锁顺序比事后 try_lock 靠谱。锁不是「加一把试试」——是跨模块的显式契约,和参数声明一样需要可审计。现场 hang 一整晚,日志里往往只有「最后一帧正常」,TSan 与 stress 测试才是合入门槛。新人合入前必须读过 LOCK_ORDER,PR 模板里加一项「是否违反锁序」。
2. 层次锁与 scoped_lock
全局顺序例:tf 锁 → map 锁 → tracker 锁 → viz 锁。所有线程按升序加锁,禁止逆序。写进 LOCK_ORDER.md,新人 PR grep Violations。map 模块与 tf 模块曾 AB-BA 死锁——统一全局顺序后 stress 测试才稳定。
std::scoped_lock lock(tf_cache_mu_, map_mu_);需要两把锁时 C++17 scoped_lock 内部 std::lock 避免手写顺序错。recursive_mutex 只在 legacy 暂时用,新代码禁止——掩盖设计问题,TSan 也帮不了你理清回调树。
3. 缩小临界区
持锁不 call 用户 callback、不做 IO、不 decode 整帧——hook 若再抢同序锁必 deadlock。文档写「回调在锁外 invoke」。锁内 pop index,锁外 process;只 swap pointer,外面处理。持锁范围最小化是延迟尖峰的第一道防线,临界区里 decode 整帧是 p99 尖峰常见根因。持锁期间禁止 call 用户 plugin hook——这是死锁最高频路径。若临界区必须拷贝大对象,考虑 shared_ptr 交换指针而非在锁内 deep copy 整张地图。
4. try_lock、shared_mutex 与 executor
实时线程 try_lock 失败则跳过本帧可视化,别 infinite block——失败限频 log。地图 query 用 shared_lock,insert scan 用 unique_lock;写饥饿时要测,极端情况改用 short critical 加 copy-on-write snapshot。MultiThreadedExecutor 加节点内 mutex 易死锁——callback group 互斥或单线程 executor 处理有共享状态的节点。condition_variable::wait 必须配 predicate 防虚假唤醒。Nav2 栈里 costmap 与 localization 争用顺序应写入 LOCK_ORDER,与 tf 缓存锁一起构成全局层次,新人 PR 必须 grep 是否逆序。
5. 与 ROS 2 回调模型
传感器 callback 持锁别 call 用户 hook——hook 里若再取 map 锁必挂。参数回调也占执行器——里面做巨型重建会像堵 service 一样拖死控制。重活进工作队列,回调只设旗标。与 lifecycle 交点:configure 可读、active 不可改的参数,描述里写明。timed_mutex try_lock_for 在控制环里失败就 skip viz,别 spin 占 CPU 让 decode 线程饿死。executor 与节点内锁的交互应在设计阶段画出来——MultiThreadedExecutor 不是「加线程就安全」,callback 仍可能逆序抢锁。
6. 文档与 code review
Nav2 模块互斥顺序写进 LOCK_ORDER.md:永远先 tf 后 map,新人 PR 必须 grep Violations。文档画 lock hierarchy 贴 wiki,code review 对照。两线程反向加锁 stress 单测应 TSan 报——合入前跑十分钟 bag replay。锁契约变更应像参数变更一样走 review:谁持有哪把锁、回调是否在锁外,应在 PR 描述里可核对。
7. 案例:logging 回调里的逆序加锁
某定位栈在持 map 锁时打 INFO,日志后端异步回调里又要 map 锁做统计——低概率 AB-BA,field 一晚 hang 一次。改成锁内只拷贝 snapshot、锁外 log 后消失。根因不是日志库,是「持锁 call 外部代码」契约缺失。教训:持锁禁止 callback、IO、用户 hook,写进 review checklist。LOCK_ORDER 文档要与代码同步更新,删锁或合并模块时先改顺序再合入。
8. 验收
- TSan;十分钟 bag replay 红则合入阻断。
- stress 两线程反向加锁单测应 TSan 报。
- LOCK_ORDER.md 与 PR grep 一致。
- try_lock 失败计数 export diagnostics。
- 持锁路径 code review 必问「是否 call 外部代码」。
mutex 是工具——层次锁、scoped_lock、锁外 callback,比随机加锁可审计得多;偶发 hang 的代价远高于多写一行 scoped_lock。锁顺序与参数声明一样,是跨模块契约,删锁或合并模块时先更新 LOCK_ORDER 再合入;偶发 hang 的排查成本远高于日常多写一行 scoped_lock。
相关
也可以看看
johan's blog