☰
从零设计一个运动控制软件:Actor 线程模型为什么是设备行业的主流
2026/10/4 12:59:44 网站建设 项目流程

本文站在设备行业架构视角,聊聊「从零设计一台多工位运动控制设备的软件」时,为什么会自然走到 Actor 线程模型这条路。不绑定具体框架,适合做半导体 Handler、贴装、检测、分拣、包装等自动化设备软件的工程师和面试参考。

一、先看清设备软件要解决的本质问题

很多人一上来就纠结「用什么框架」「要不要上 ROS」「要不要用 PLC + 上位机」。其实先退一步,看清楚设备软件到底要解决什么。

一台典型的自动化设备(Handler、贴片机、分选机、测试分拣线)通常有这几个特征:

  1. 多工位并行:上料、传输、测试、下料同时进行,不是一条直线走到底。
  2. 强硬件依赖:电机、气缸、真空、传感器、相机、测试机,时序错了一步就撞机或废料。
  3. 长周期流程:一个料从进到出可能几十秒到几分钟,中间要等测试、等真空、等下一工位就绪。
  4. 安全优先:任何一处异常都要能快速停机、不让料继续往下走。
  5. 可维护可扩展:今天 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 不动

停机逻辑各写各的

统一StopAll,一个入口停全部

步骤推进混乱

每个 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 模型就是第三步的产物,它的核心是:

一个独立执行单元 = 一个长期线程 + 一个状态机 + 一组协作接口。

它解决了设备软件最头疼的三个问题:

  1. 状态归属:状态在 Actor 内,不散落全局。
  2. 模块协作:通过占用、握手、广播三种协议,边界清晰。
  3. 统一管控:注册表 + 统一接口,一键启停、一键回零、一键结批。

设备行业用 Actor 不是因为保守,而是因为设备软件的「多工位并行 + 状态机 + 硬件时序 + 安全停机」天然适合这种模型。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询