尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

设备软件的线程安全:锁顺序、死锁规避、运动线程的边界

设备软件的线程安全:锁顺序、死锁规避、运动线程的边界 这篇解决一个问题多 Actor 并行时怎么加锁、怎么避死锁、运动线程里哪些事绝对不能做。一、痛点设备软件的死锁不是「写错了 mutex」那么简单多线程设备软件里死锁常常长这样手臂线程握着轴相关资源同步回调里去抢 Buffer 占用Buffer 线程握着 Buffer流程里又调手臂Move去抢轴资源两边各拿一把互相等机台「假死」。现场表现通常不是立刻崩溃而是某个工位卡住不动别的工位还在跑残料继续流动日志里最后一条停在WaitArrive或SetUsedBy重启后一切正常很难复现。根因往往不是「忘了 unlock」而是 锁顺序交叉 运动线程里做了跨模块重活。二、设计思路先统一「锁是什么」设备软件里的「锁」不止mutex至少有三类类型例子管什么互斥锁std::mutex、临界区保护共享数据资源占用Station::SetUsedBy谁占用工位硬件占用轴正在 Move/WaitArrive谁在动这个轴死锁可以发生在任何两类之间交叉。所以规则不能只写「mutex 按名字排序」还要规定跨模块调用时你手里能不能还握着别的资源。核心原则只有三条全局统一加锁/占用顺序握锁时不做跨模块回调运动线程只做运动业务协调另开路径三、协议统一锁顺序思路工位占用锁Buffer / Channel / Press → 业务数据锁Tray 信息 / Flag → 轴/运动相关锁任何线程加锁或占用时只能按这个方向走禁止反过来。关键片段// 允许先占工位再动轴 if (!st-SetUsedBy(this)) return; auto ret MoveToStation(st, ArmMotion::Pick); // 禁止先握轴相关资源再回头抢工位 // WaitArrive() 里 emit → 订阅者 SetUsedBy(buffer)边界顺序是约定不是编译器帮你检查的靠 Code Review 和静态规则。临时插入「只加一把锁做个快照」可以但不要在持锁时发起新的占用请求。如果必须反序拿锁用try_lock/ 失败重试并设超时不能无限等。四、协议握锁时不做跨模块回调痛点同步观察者最容易踩死锁线程 A持轴锁 → emit(到位) → 订阅者抢 Buffer线程 B持 Buffer → 调 Move → 抢轴锁emit如果是同步的订阅者代码就在 A 的调用栈上跑还握着 A 的锁。思路事件发布只入队松锁后再处理。void OnArrive() { // 差握着资源直接回调业务 // placeToBuffer(); // 好只投递轻量事件 eventQueue.push({Event::Arrived, stationId}); }状态机下一拍取出事件再做SetUsedBy/Place。这样运动线程立刻返回业务逻辑在自己的步骤里执行锁持有时间短交叉概率大幅下降。边界UI 弹窗、数据库写入、网络上报一律不要放在运动线程同步路径。报警可以emit但槽函数应尽快返回写库/弹窗放到报警线程或 UI 线程。「轻量」标准不抢别的工位、不动轴、不Sleep、不弹窗。五、协议运动线程的边界痛点运动线程一旦做业务协调重入和死锁一起爆// 危险Move 还没返回内部回调又调 Move MoveToPos(pos); // 内部 emit → 订阅者又调用 MoveToPos / WorkFlow调用栈叠两层状态机n_step被重入改写现场极难查。思路给运动线程划死边界可以做MoveTo/WaitArrive/ 读编码器开关真空、读传感器投递「到位/失败」事件更新本模块局部状态不可以做抢其它工位占用调其它 Actor 的状态机弹模态对话框同步写数据库 / 发网络包在持锁时emit重业务回调关键片段case Step::MoveToBackup: { auto ret MoveToPos(pos_backup); if (!ret.IsOK()) { Alarm(ret); ControllerStop(); break; } // 只投递不在这里 PlaceTray / 再 Move PostReadyToSend({stationId, trayId}); step Step::WaitTake; break; }边界示教/手动点动可以短流程同步因为通常单线程、无并行抢资源。自动运行模式必须守边界「方便」换来的是偶发死锁。如果历史代码已是同步回调最小改造是回调里只push事件业务仍回状态机。六、三种死锁形态与对应解法形态 1锁顺序交叉A: 轴锁 → Buffer 锁B: Buffer 锁 → 轴锁解法统一顺序或运动完成后再占工位。形态 2回调重入Move → emit → 回调里再 Move解法禁止同步重入事件入队下拍处理。形态 3停机路径二次同步Alarm → Stop → 上报失败再 Alarm → UI/写库卡住同一线程解法停机路径副作用异步化二次报警降级或丢弃避免停机套停机。七、实用规则清单可直接贴进规范资源顺序工位占用 → 业务数据 → 运动资源禁止反向。持锁时长临界区只做读写共享数据不做 IO、不做运动、不做回调。占用配对SetUsedBy成功后所有退出路径含报警必须SetNotUsedBy。等待必超时WaitArrive、握手Wait、抢锁重试都要超时 报警。运动线程禁重活只运动 投递事件。同步 emit 默认危险能异步就异步同步回调里禁止跨模块抢锁。停机可响应硬件等待拆成非阻塞轮询确保Stop标志能被看到。八、什么时候「看起来像锁问题」其实不是有些卡死被误判为死锁状态机忘跳步一直停在某case其实没锁。握手对方永不 Ready像死等实则协议漏发事件。占用未释放像死锁实则资源泄漏。阻塞 Wait 不检查 Stop像卡死实则停机标志看不到。排查顺序建议看最后日志步骤 → 查占用者是谁 → 查是否在 Wait → 查调用栈有无交叉抢锁先分清「真死锁 / 资源泄漏 / 协议丢失 / 阻塞不可中断」再动手改锁。九、可复用结论设备软件的线程安全核心不是多加锁而是 统一顺序 缩短持锁 禁止跨模块同步回调。「锁」包含 mutex、工位占用、轴占用交叉发生在任两类之间。运动线程边界要写进规范只运动、只投递不协调、不弹窗、不写库。死锁高发在「到位回调里抢业务资源」事件入队是最有效的解耦手段。停机路径必须可中断、可清理占用否则「假死」和「真死锁」一样致命。这些规则可迁移到 Handler、贴装、分选、检测等任何多工位并行设备。上一篇讲协作协议这篇讲协议落地时的线程边界。两者合起来才是多 Actor 设备能稳定跑的底座。
返回列表