作:資源占用、就緒握手、異常廣播三種協(xié)議)
這篇解決一個核心問題設(shè)備軟件里幾十個 Actor 各跑各的線程它們怎么配合把一塊料從進到出搬完而不互相踩腳。一、痛點多線程協(xié)作最容易踩的三個坑設(shè)備軟件里一個料從上料到下料要經(jīng)過上料站、搬運手、Buffer、測試臺、下料手好幾個 Actor。每個 Actor 一個線程各自跑自己的狀態(tài)機。一旦協(xié)作設(shè)計不好三個坑必踩。坑一搶同一資源撞機。 兩個手臂同時往一個 Buffer 放料誰也沒問對方在不在結(jié)果撞在一起。坑二對方?jīng)]準備好就送。 A 把料取走了B 還沒就位A 就往下放料掉地上。坑三一個掛了全亂套。 A 報警停了B 不知道還在往 A 送料殘料越堆越多。這三個坑分別對應三種協(xié)作協(xié)議資源占用、就緒握手、異常廣播。下面逐個講。二、協(xié)議一資源占用——誰在用誰負責痛點// 錯誤做法 bool bufferBusy false; // Actor AbufferBusy true; 用完 bufferBusy false; // Actor Bif (!bufferBusy) 用它問題在于bufferBusy只表示「忙不忙」不表示「誰在用」。A 設(shè)了trueB 怎么知道是 A 在用還是 C 在用A 掛了忘改回falseB 永遠等不到。設(shè)計思路給共享資源加一個「占用者」字段記錄是誰在用而不是只記「忙不忙」。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; } };關(guān)鍵點只允許兩種情況拿到資源空閑時任何人能拿或者已經(jīng)是你占著重入安全。其他人想拿返回false下一拍再試。釋放時必須驗證身份SetNotUsedBy(a)里要檢查m_owner a不是你占的你不能釋放。防止 A 掛了之后 B 誤釋放 A 的占用。用法// Actor 想用 Buffer if (buffer-SetUsedBy(this)) { // 拿到了干活 PickFrom(buffer); buffer-SetNotUsedBy(this); // 用完釋放 } else { // 沒拿到下一拍再試 break; }邊界與坑坑忘記釋放。 Actor 拿到資源后報警退出沒走SetNotUsedBy資源被永久鎖死。對策報警處理流程里統(tǒng)一檢查并釋放該 Actor 占的所有資源。坑重入判斷要小心。m_owner a允許同一個 Actor 重復拿是為了避免「拿了之后狀態(tài)機跳步又來拿一次」的死鎖。但如果你不希望重入去掉這個條件即可。不適合的場景如果資源占用時間極短幾毫秒用 mutex 直接鎖更簡單不必走占用協(xié)議。占用協(xié)議適合「占用幾秒到幾十秒」的工位級資源。三、協(xié)議二就緒握手——你準備好我才送痛點A 要把料給 B得確認 B 能接。最樸素的寫法是 A 輪詢 B 的 bool// 錯誤做法 while (!B-isReady) Sleep(1); // A 死等 B問題一堆isReady裸 bool 無鎖有競態(tài)A 空轉(zhuǎn)查 bool 浪費 CPUisReady只能表示「準備好了」不能帶「準備接什么料、接幾顆」這類數(shù)據(jù)。設(shè)計思路握手要解決三件事對方知道我準備好了、我能帶上數(shù)據(jù)、等待方不空轉(zhuǎn)。有兩種實現(xiàn)按項目階段選。實現(xiàn)一輪詢式老項目常見// B 側(cè) void SetReadyToRecv(bool ready) { m_bReadyToRecv ready; } bool IsReadyToRecv() { return m_bReadyToRecv; } // A 側(cè) if (B-IsReadyToRecv()) { // B 能接開始送 }簡單直接但有競態(tài)和空轉(zhuǎn)問題。適合老項目改造、過渡階段不建議新項目用。實現(xiàn)二條件變量 帶數(shù)據(jù)推薦struct HandoffEvent { enum Type { ReadyToSend, ReadyToRecv, Done } type; string fromStation; string trayId; // ... 可帶任意數(shù)據(jù) }; 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阻塞等通知不空轉(zhuǎn) HandoffEvent ev; if (!channel.Wait(HandoffEvent::Done, ev, 30000)) Alarm(交接超時);邊界與坑坑輪詢式里 bool 沒加鎖。 多線程讀寫裸 bool 理論上是未定義行為實際在 x86 上多數(shù)沒事但不保證。至少用atomicbool或加鎖。坑條件變量握手里別在 Post 時做重活。Post只做「入隊 notify」不要在Post里調(diào)運動、搶鎖否則等待方被叫醒后又要等 Post 里的鎖容易死鎖。坑超時一定要有。Wait不帶超時對方掛了你就永遠等。設(shè)備軟件里所有等待都要有超時 報警。不適合的場景如果 A 和 B 在同一個線程里跑比如示教模式不需要握手直接順序調(diào)用即可。握手是跨線程協(xié)作才需要的。四、協(xié)議三異常廣播——一處故障全機響應痛點設(shè)備運行中任何一個 Actor 都可能遇到故障軸超時、真空失敗、氣缸不到位。如果只讓故障 Actor 自己停其它 Actor 還在往里送料殘料堆積、二次碰撞、批次數(shù)據(jù)錯亂。設(shè)計思路需要一個全局入口任意 Actor 都能觸發(fā)由總控統(tǒng)一停掉所有 Actor。class Actor { static Actor* g_Controller; static void ControllerStop() { if (g_Controller) g_Controller-Stop(); } };// 總控的 Stop void CControl::Stop() { StopMotionCard(); // 先停運動卡防止軸繼續(xù)動 Actor::StopAllActors(); // 廣播停所有 Actor SetMachineState(PAUSE); // 改整機狀態(tài) }用法任意 Actor 發(fā)現(xiàn)嚴重故障if (!ret.IsOK()) { Alarm(ret); // 報警 Actor::ControllerStop(); // 觸發(fā)整機停 break; // 自己也跳出當前步 }關(guān)鍵點停機順序很重要先停運動卡硬件層再停 Actor軟件層最后改狀態(tài)。反過來會導致 Actor 還在跑時軸已經(jīng)停了狀態(tài)機卡在中間。報警和停機分開調(diào)。不是所有報警都要停機真空可忽略的失敗只報警不停機軸超時這類嚴重故障才報警 停機。是否停機由調(diào)用方?jīng)Q定不要寫死在Alarm里。邊界與坑坑停機后忘釋放資源。 Actor 占著 Buffer 報警停了沒走SetNotUsedBy重啟后 Buffer 還是被占。對策Stop流程里統(tǒng)一清理該 Actor 的所有占用。坑停機里又觸發(fā)報警。Stop里上報 SECS 事件失敗又調(diào)AlarmAlarm又想emit信號信號槽里又操作 UIUI 線程正好在等這個 Actor 停……死鎖。對策停機流程里的二次報警要吞掉或異步化不要在停機路徑上再產(chǎn)生同步副作用。坑停機不是瞬時的。Stop只是置標志Actor 狀態(tài)機要走到Sleep(1)那一拍才會檢查到。如果某個 Actor 正卡在WaitArrive阻塞里它不會立刻停。所以前面說阻塞等待要拆成非阻塞輪詢這也是原因之一。不適合的場景可恢復的輕微異常某格真空沒吸到但可跳過不該觸發(fā)整機停局部處理即可。ControllerStop只留給「不停會有安全風險」的故障。五、三種協(xié)議怎么配合實際設(shè)備里三種協(xié)議是疊加用的不是三選一。一個完整的搬運流程1. Actor A 想從 Buffer 取料→ 先 SetUsedBy(this) 占住 Buffer [協(xié)議一資源占用]2. 取完料通知 Actor B「我送過去了」→ channel.Post(ReadyToSend, {料號, 位置}) [協(xié)議二就緒握手]3. Actor B 收到通知確認能接→ SetUsedBy(this) 占住放料站 [協(xié)議一]→ channel.Wait(ReadyToSend, ev) [協(xié)議二]4. 任意一步發(fā)現(xiàn)軸超時→ Alarm ControllerStop [協(xié)議三異常廣播]→ 所有 Actor 停資源釋放三種協(xié)議各管一段占用管互斥、握手管同步、廣播管安全。缺任何一層都會出問題。六、可復用結(jié)論資源占用共享工位用「占用者指針」而不是裸 bool記錄誰在用、誰能釋放。就緒握手跨線程交接用條件變量 帶數(shù)據(jù)的事件不用裸 bool 輪詢所有等待加超時。異常廣播嚴重故障走全局ControllerStop總控統(tǒng)一停所有 Actor停機順序先硬件后軟件。報警和停機分開不是所有報警都停機是否停機由調(diào)用方?jīng)Q定。停機后清理資源Stop流程里統(tǒng)一釋放該 Actor 的所有占用防止重啟后資源被鎖死。阻塞等待拆非阻塞狀態(tài)機里的硬件等待拆成輪詢保證停機標志能及時響應。這三層協(xié)議是設(shè)備軟件多線程協(xié)作的骨架。理解了它們設(shè)計新設(shè)備時就知道「這一步該用哪種協(xié)議」