本文站在设备行业架构视角,聊聊「从零设计一台多工位运动控制设备的软件」时,为什么会自然走到 Actor 线程模型这条路。不绑定具体框架,适合做半导体 Handler、贴装、检测、分拣、包装等自动化设备软件的工程师和面试参考。
一、先看清设备软件要解决的本质问题
很多人一上来就纠结「用什么框架」「要不要上 ROS」「要不要用 PLC + 上位机」。其实先退一步,看清楚设备软件到底要解决什么。
一台典型的自动化设备(Handler、贴片机、分选机、测试分拣线)通常有这几个特征:
- 多工位并行:上料、传输、测试、下料同时进行,不是一条直线走到底。
- 强硬件依赖:电机、气缸、真空、传感器、相机、测试机,时序错了一步就撞机或废料。
- 长周期流程:一个料从进到出可能几十秒到几分钟,中间要等测试、等真空、等下一工位就绪。
- 安全优先:任何一处异常都要能快速停机、不让料继续往下走。
- 可维护可扩展:今天 2 条料道,明天加到 4 条;今天测一种产品,明天换型。
这五条决定了设备软件不是「Web 后端那种请求-响应」模型,也不是「桌面软件那种事件驱动 UI」模型,而是一种多执行单元 + 各自状态机 + 资源互斥 + 异常广播停机的模型。
二、从零设计时你会撞上的三道坎
如果你真的从零写一台设备软件,大概率会经历这三个阶段。
第一道坎:单线程写不下去
最朴素的写法是一个大循环:
while (running) { 上料(); 搬运1(); 测试(); 搬运2(); 下料(); }很快发现:测试要 10 秒,搬运 1 只能干等,产能上不去。于是你想让搬运和测试并行。
第二道坎:多线程 + 共享变量,越写越乱
开了几个线程,用一堆bool互相通知:
bool bufferReady = false;
bool armBusy = false;
// 线程 A 改 bufferReady,线程 B 轮询
这种代码写到第 5 个模块就开始失控:
- 谁改了谁的状态搞不清;
- 一个 bool 既表示「就绪」又表示「完成」,语义混乱;
- 加新模块要改一堆老代码;
- 死锁、竞态、漏停机层出不穷。
笔者就是吃过这种亏,后边才开始反思有没有更方便的写法
第三道坎:你开始抽象「执行单元」
痛够了你会意识到:每个模块(上料站、搬运手、测试台、下料站)其实有共同的抽象:
- 有自己的生命周期(启动、运行、暂停、停止、回零)。
- 有自己的状态/步骤(取料中、放料中、等待中、故障)。
- 需要和其它模块协作(等对方就绪、通知对方完成)。
- 出问题要能统一停机。
一旦你把这个抽象提炼出来,给它起个名字——Actor——你就走到了设备行业的主流线程模型。
三、Actor 在设备软件里到底是什么
注意,这里说的 Actor 不是 Akka/ Erlang 那种「信箱消息传递」的 Actor 模型。设备行业说的 Actor 更朴素:
一个独立执行单元 = 一个长期线程 + 一个状态机 + 一组对外协作接口。
它有这几个关键属性。
1. 一个 Actor 一个线程
每个 Actor(上料站、搬运手、测试台……)独占一个线程,线程里跑它的WorkFlow()主循环。线程生命周期和 Actor 对象绑定,不是「取一个任务做完就退出」的短任务。
2. 状态机驱动
Actor 的WorkFlow不是一个线性函数,而是:
while (running) { wait_if_not_started(); if (faulted) handle_fault(); switch (step) { case 取料: ...; step = 搬运; break; case 搬运: ...; step = 放料; break; case 放料: ...; step = 完成; break; } }每个 Actor 自己推进自己的步骤,不依赖外部调度。
3. 统一的生命周期接口
所有 Actor 都能被统一地:启动、停止、暂停、回零、结批。这样总控可以一键StopAll()、RunAll()、HomeAll(),不用关心每个模块内部细节。
4. 资源占用与互斥
工位、Buffer、压台这些共享资源,Actor 之间通过「占用/释放」协议互斥,而不是裸 bool 轮询。
5. 异常可广播停机
任意 Actor 发现严重故障,调用一个全局入口(比如ControllerStop()),总控停掉所有 Actor,避免残料继续流动。
四、Actor 模型解决了第二道坎里的哪些问题
回到前面「多线程 + 共享 bool」的混乱,Actor 模型对症下药:
| 之前的问题 | Actor 模型的解法 |
|---|---|
状态散落在全局 bool | 状态封装在 Actor 内部,对外只暴露接口 |
模块间互相直接改对方变量 | 通过占用协议、就绪握手、事件通知协作 |
加新模块要改老代码 | 新 Actor 注册进注册表,老 Actor 不动 |
停机逻辑各写各的 | 统一 |
步骤推进混乱 | 每个 Actor 自己的状态机,边界清晰 |
本质上,Actor 把「谁的状态、谁推进、谁负责停」这三件事绑在了一起,消除了「状态归属不清」这个最大的混乱源。
五、Actor 之间怎么协作:三种典型手段
设备软件里 Actor 不是孤岛,它们要配合完成「料从进到出」。常见的协作手段有三种,理解这三种就够应付大多数场景。
手段一:资源占用协议(互斥)
最典型的场景:两个手臂抢同一个 Buffer。
做法是给共享资源加一个「占用者」字段:
bool Station::SetUsedBy(Actor* a) { lock(); if (m_owner == nullptr || m_owner == a) { m_owner = a; return true; } return false; }Actor 想用资源先SetUsedBy(this),用完SetNotUsedBy(this)。抢不到就等下一拍。这比裸 bool 强在:占用关系明确、可查询、可释放、可防止重入。
手段二:就绪握手(同步)
A 要把料给 B,得等 B 说「我能接」。这种「准备好」握手通常有两种实现:
- 轮询式:A 查
B.IsReadyToRecv(),B 改自己的标志。简单但有延迟、易竞态。 - 事件/条件变量式:B 准备好后
Post一个带数据的事件,A 阻塞Wait。无空转、能带数据、更解耦。
工业里两种都有,前者老项目多,后者新项目更推荐。
手段三:异常广播(停机)
任意 Actor 发现严重故障,走统一上报路径:
Actor::Alarm(error); Actor::ControllerStop(); // 全局入口停整机总控收到后停掉所有 Actor,避免「A 挂了 B 还在往里送料」的二次事故。
六、为什么不用线程池
线程池和 Actor 模型看起来都是「多线程」,但适用场景完全不同。
| 维度 | 线程池 | Actor 一对象一线程 |
|---|---|---|
任务粒度 | 短任务,做完还线程 | 长期状态机,常驻 |
状态归属 | 任务无状态或状态外置 | 状态在 Actor 内 |
调度 | 框架调度,任务排队 | 每个 Actor 自驱 |
适合场景 | Web 请求、图像处理、IO 密集 | 硬件流程、状态机、实时控制 |
取消 | 任务级取消 | Actor 级停止标志 |
设备软件里每个模块是长期运行的状态机,不是「处理完就结束」的短任务。把状态机塞进线程池,反而要额外管理「哪个线程现在跑哪个 Actor 的哪一步」,复杂度更高。
所以设备行业几乎清一色用「一 Actor 一线程」,而不是线程池。这不是落后,是场景决定的。
七、Actor 模型的设计要点(可复用清单)
如果让你主导一台新设备的软件架构,按 Actor 模型落地时抓住这几条:
1. 抽象一个公共基类
所有执行单元继承同一个基类,提供统一接口:
class Actor { void Run(); void Stop(); void Home(); void EndLot(); bool IsRun(); bool IsFaulted(); virtual void WorkFlow() = 0; // 子类实现自己的状态机 };2. 用注册表管理所有 Actor
static vector<Actor*> g_Actors; // 启动时 for (auto* a : g_Actors) a->Run(); // 停机时 for (auto* a : g_Actors) a->Stop();注册表让你能一键广播,不用在每个地方手动调一遍。
3. 状态用枚举 + switch,不要用一堆 bool
enum class Step { 取料, 搬运, 放料, 完成 }; Step step = Step::取料;而不是bool picking, bool moving, bool placing。枚举状态机天然互斥,bool 组合会产生非法状态。
4. 资源互斥用占用协议,不要用裸 bool
共享工位加owner字段,SetUsedBy/ SetNotUsedBy配对使用。
5. 异常停机走全局入口
任意 Actor 能触发整机停,不要让每个模块自己实现停机逻辑。
6. 启停标志和线程存在标志分开
bRunFlag:业务运行中(可暂停恢复)bThreadExist:线程是否存活(退出用)
两个标志分开,才能做到「暂停不停线程,停机才退线程」。
八、实际工程中的取舍与边界
Actor 模型不是银弹,它也有边界。
适合 Actor 的
- 工位级模块:上料站、搬运手、测试台、下料站。
- 长周期状态机:一个料从进到出要几十秒以上。
- 需要并行多个同类模块:左右两条料道、多只手臂。
不适合 Actor 的
- 短任务并发:图像处理、数据库写入、网络通信。这些用线程池或异步更合适。
- 高频实时控制:伺服环、运动学插补。这些要在实时线程或控制卡里做,不在 Actor 层。
- UI 刷新:用 UI 线程事件驱动,不要塞进 Actor。
一个成熟设备软件通常是混合模型:Actor 管「业务流程和工位协作」,线程池/异步管「短任务和 IO」,实时任务管「伺服和插补」。不要强求一种模型通吃。
九、总结
从零设计一台运动控制设备软件,你会自然走过「单线程 → 多线程乱 → 抽象执行单元」这三步。Actor 模型就是第三步的产物,它的核心是:
一个独立执行单元 = 一个长期线程 + 一个状态机 + 一组协作接口。
它解决了设备软件最头疼的三个问题:
- 状态归属:状态在 Actor 内,不散落全局。
- 模块协作:通过占用、握手、广播三种协议,边界清晰。
- 统一管控:注册表 + 统一接口,一键启停、一键回零、一键结批。
设备行业用 Actor 不是因为保守,而是因为设备软件的「多工位并行 + 状态机 + 硬件时序 + 安全停机」天然适合这种模型。