
这篇解决一个核心问题设备软件里几十个 Actor 各跑各的线程它们怎么配合把一块料从进到出搬完而不互相踩脚。一、痛点多线程协作最容易踩的三个坑设备软件里一个料从上料到下料要经过上料站、搬运手、Buffer、测试台、下料手好几个 Actor。每个 Actor 一个线程各自跑自己的状态机。一旦协作设计不好三个坑必踩。坑一抢同一资源撞机。 两个手臂同时往一个 Buffer 放料谁也没问对方在不在结果撞在一起。坑二对方没准备好就送。 A 把料取走了B 还没就位A 就往下放料掉地上。坑三一个挂了全乱套。 A 报警停了B 不知道还在往 A 送料残料越堆越多。这三个坑分别对应三种协作协议资源占用、就绪握手、异常广播。下面逐个讲。二、协议一资源占用——谁在用谁负责痛点// 错误做法 bool bufferBusy false; // Actor AbufferBusy true; 用完 bufferBusy false; // Actor Bif (!bufferBusy) 用它问题在于bufferBusy只表示「忙不忙」不表示「谁在用」。A 设了trueB 怎么知道是 A 在用还是 C 在用A 挂了忘改回falseB 永远等不到。设计思路给共享资源加一个「占用者」字段记录是谁在用而不是只记「忙不忙」。class Station { Actor* m_owner nullptr; // 谁在占用null 表示空闲 mutex m_mtx; bool SetUsedBy(Actor* a) { lock_guardmutex lk(m_mtx); if (m_owner nullptr || m_owner a) { m_owner a; return true; } return false; } void SetNotUsedBy(Actor* a) { lock_guardmutex lk(m_mtx); if (m_owner a) m_owner nullptr; } };关键点只允许两种情况拿到资源空闲时任何人能拿或者已经是你占着重入安全。其他人想拿返回false下一拍再试。释放时必须验证身份SetNotUsedBy(a)里要检查m_owner a不是你占的你不能释放。防止 A 挂了之后 B 误释放 A 的占用。用法// Actor 想用 Buffer if (buffer-SetUsedBy(this)) { // 拿到了干活 PickFrom(buffer); buffer-SetNotUsedBy(this); // 用完释放 } else { // 没拿到下一拍再试 break; }边界与坑坑忘记释放。 Actor 拿到资源后报警退出没走SetNotUsedBy资源被永久锁死。对策报警处理流程里统一检查并释放该 Actor 占的所有资源。坑重入判断要小心。m_owner a允许同一个 Actor 重复拿是为了避免「拿了之后状态机跳步又来拿一次」的死锁。但如果你不希望重入去掉这个条件即可。不适合的场景如果资源占用时间极短几毫秒用 mutex 直接锁更简单不必走占用协议。占用协议适合「占用几秒到几十秒」的工位级资源。三、协议二就绪握手——你准备好我才送痛点A 要把料给 B得确认 B 能接。最朴素的写法是 A 轮询 B 的 bool// 错误做法 while (!B-isReady) Sleep(1); // A 死等 B问题一堆isReady裸 bool 无锁有竞态A 空转查 bool 浪费 CPUisReady只能表示「准备好了」不能带「准备接什么料、接几颗」这类数据。设计思路握手要解决三件事对方知道我准备好了、我能带上数据、等待方不空转。有两种实现按项目阶段选。实现一轮询式老项目常见// B 侧 void SetReadyToRecv(bool ready) { m_bReadyToRecv ready; } bool IsReadyToRecv() { return m_bReadyToRecv; } // A 侧 if (B-IsReadyToRecv()) { // B 能接开始送 }简单直接但有竞态和空转问题。适合老项目改造、过渡阶段不建议新项目用。实现二条件变量 带数据推荐struct HandoffEvent { enum Type { ReadyToSend, ReadyToRecv, Done } type; string fromStation; string trayId; // ... 可带任意数据 }; class HandoffChannel { queueHandoffEvent q_; mutex mtx_; condition_variable cv_; public: void Post(HandoffEvent ev) { { lock_guardmutex lk(mtx_); q_.push(move(ev)); } cv_.notify_one(); // 叫醒等待方 } bool Wait(HandoffEvent::Type want, HandoffEvent out, int timeoutMs) { unique_lockmutex lk(mtx_); bool ok cv_.wait_for(lk, chrono::milliseconds(timeoutMs), [] { return !q_.empty() q_.front().type want; }); if (!ok || q_.empty()) return false; out q_.front(); q_.pop(); return true; } };用法// A干完活通知 channel.Post({HandoffEvent::Done, ArmTray, TRAY_001}); // B阻塞等通知不空转 HandoffEvent ev; if (!channel.Wait(HandoffEvent::Done, ev, 30000)) Alarm(交接超时);边界与坑坑轮询式里 bool 没加锁。 多线程读写裸 bool 理论上是未定义行为实际在 x86 上多数没事但不保证。至少用atomicbool或加锁。坑条件变量握手里别在 Post 时做重活。Post只做「入队 notify」不要在Post里调运动、抢锁否则等待方被叫醒后又要等 Post 里的锁容易死锁。坑超时一定要有。Wait不带超时对方挂了你就永远等。设备软件里所有等待都要有超时 报警。不适合的场景如果 A 和 B 在同一个线程里跑比如示教模式不需要握手直接顺序调用即可。握手是跨线程协作才需要的。四、协议三异常广播——一处故障全机响应痛点设备运行中任何一个 Actor 都可能遇到故障轴超时、真空失败、气缸不到位。如果只让故障 Actor 自己停其它 Actor 还在往里送料残料堆积、二次碰撞、批次数据错乱。设计思路需要一个全局入口任意 Actor 都能触发由总控统一停掉所有 Actor。class Actor { static Actor* g_Controller; static void ControllerStop() { if (g_Controller) g_Controller-Stop(); } };// 总控的 Stop void CControl::Stop() { StopMotionCard(); // 先停运动卡防止轴继续动 Actor::StopAllActors(); // 广播停所有 Actor SetMachineState(PAUSE); // 改整机状态 }用法任意 Actor 发现严重故障if (!ret.IsOK()) { Alarm(ret); // 报警 Actor::ControllerStop(); // 触发整机停 break; // 自己也跳出当前步 }关键点停机顺序很重要先停运动卡硬件层再停 Actor软件层最后改状态。反过来会导致 Actor 还在跑时轴已经停了状态机卡在中间。报警和停机分开调。不是所有报警都要停机真空可忽略的失败只报警不停机轴超时这类严重故障才报警 停机。是否停机由调用方决定不要写死在Alarm里。边界与坑坑停机后忘释放资源。 Actor 占着 Buffer 报警停了没走SetNotUsedBy重启后 Buffer 还是被占。对策Stop流程里统一清理该 Actor 的所有占用。坑停机里又触发报警。Stop里上报 SECS 事件失败又调AlarmAlarm又想emit信号信号槽里又操作 UIUI 线程正好在等这个 Actor 停……死锁。对策停机流程里的二次报警要吞掉或异步化不要在停机路径上再产生同步副作用。坑停机不是瞬时的。Stop只是置标志Actor 状态机要走到Sleep(1)那一拍才会检查到。如果某个 Actor 正卡在WaitArrive阻塞里它不会立刻停。所以前面说阻塞等待要拆成非阻塞轮询这也是原因之一。不适合的场景可恢复的轻微异常某格真空没吸到但可跳过不该触发整机停局部处理即可。ControllerStop只留给「不停会有安全风险」的故障。五、三种协议怎么配合实际设备里三种协议是叠加用的不是三选一。一个完整的搬运流程1. Actor A 想从 Buffer 取料→ 先 SetUsedBy(this) 占住 Buffer [协议一资源占用]2. 取完料通知 Actor B「我送过去了」→ channel.Post(ReadyToSend, {料号, 位置}) [协议二就绪握手]3. Actor B 收到通知确认能接→ SetUsedBy(this) 占住放料站 [协议一]→ channel.Wait(ReadyToSend, ev) [协议二]4. 任意一步发现轴超时→ Alarm ControllerStop [协议三异常广播]→ 所有 Actor 停资源释放三种协议各管一段占用管互斥、握手管同步、广播管安全。缺任何一层都会出问题。六、可复用结论资源占用共享工位用「占用者指针」而不是裸 bool记录谁在用、谁能释放。就绪握手跨线程交接用条件变量 带数据的事件不用裸 bool 轮询所有等待加超时。异常广播严重故障走全局ControllerStop总控统一停所有 Actor停机顺序先硬件后软件。报警和停机分开不是所有报警都停机是否停机由调用方决定。停机后清理资源Stop流程里统一释放该 Actor 的所有占用防止重启后资源被锁死。阻塞等待拆非阻塞状态机里的硬件等待拆成轮询保证停机标志能及时响应。这三层协议是设备软件多线程协作的骨架。理解了它们设计新设备时就知道「这一步该用哪种协议」