写在前面:一个用了很多年的"老伙计"
做 LabVIEW 开发这些年,大大小小的项目做过不少——有只有一个前面板、几个按钮的小工具,也有十几个功能模块、跨多个 VI 并行跑的产线测试系统,更有行业产品级的软件系统。
框架换来换去,最后沉淀下来一直在用的,就是这套自己改出来的 AMCLite。
它脱胎于 NI 官方的 AMC(Asynchronous Message Communication)参考库。原生 AMC 很经典,但用久了就会发现几个绕不开的痛点:消息值是平化字符串,传个数组、簇、波形都得先 Flatten 再 Unflatten,写一堆胶水代码;为了支持跨网络通信引入了 UDP 和 Dispatcher,而 90% 的项目其实根本不需要跨机器通信,白白背着一堆用不上的依赖;想跨 VI 发消息,还得先把队列引用一层层传过去。
于是我把它做了减法,也做了加法:
-
减法:砍掉 UDP / Dispatcher / 网络相关的全部内容,只保留最核心的本地异步消息机制,整个库变得非常轻;
-
加法:消息值由平化字符串改为 Variant,任意 LabVIEW 数据类型直接扔进去,取出来强制转换即可;
-
加法:新增按队列名寻址的全局消息发送,不需要传递队列引用,任意 VI 之间都能直接对话。
改完之后,从简单的单循环状态机,到多线程并行的大型系统,这一套都能兼容着往下用。小项目用它不觉得重,大项目用它不觉得不够——这大概就是我一直没换的原因。
这篇文章把 AMCLite 的功能完整梳理一遍。如果你还不了解原生 AMC 的设计思想,建议先看我之前那篇 《LabVIEW教程——AMC异步通信框架详解》,本文会默认你对 QMH(队列消息处理器)设计模式有基本概念。
AMCLite工具包已打包好,博主主页简介提供AMCLite工具包下载。
一、AMCLite 是什么
一句话概括:AMCLite 是一个基于 LabVIEW 原生队列的、以 Variant 为消息载体的轻量级本地异步消息通信框架。
它解决的核心问题是:让程序中各个独立运行的部分(线程)之间,能够低耦合地互相传递命令、状态和数据。
这里说的"线程",指的是程序里那些持续运行的部分,通常表现为一个 While 循环:
-
用户界面(UI)循环
-
数据采集 / IO 处理循环
-
仪器通信循环
-
数据记录 / 存盘循环
-
状态机控制循环
这些模块彼此需要交流:UI 告诉采集循环"开始测试",采集循环告诉记录循环"这一帧数据拿去存",任何一个模块出错要能通知主控循环"我挂了,赶紧退出"。
AMCLite 就是它们之间的"邮政系统"。
与原生 AMC 的差异对照
| 对比项 | 原生 AMC | AMCLite |
|---|---|---|
| 消息值数据类型 | 平化字符串(Flattened String) | Variant |
| 传递复杂数据 | 需手动 Flatten / Unflatten | 直接连线,取出时强制转换 |
| 网络通信 | 支持,基于 UDP + Dispatcher | 已移除 |
| 跨 VI 通信 | 需传递队列引用,或依赖 Dispatcher | 按队列名直接寻址(Global 系列 VI) |
| 依赖复杂度 | 较重(含网络、注册表、Dispatcher 等) | 极简,只依赖 LabVIEW 原生队列 |
| 适用场景 | 本地 + 分布式 | 单机本地(同一 LabVIEW 应用实例内) |
需要明确说明的是:AMCLite 不做跨网络通信。如果你的项目确实需要跨设备传消息,请用原生 AMC,或者用 Network Stream、STM 等更合适的方案。AMCLite 的定位就是把"单机内部的异步通信"这一件事做到足够顺手。
什么时候用它
AMCLite 的适用面很广,下面这些场景都可以放心用:
-
低频、不定时产生的事件:用户点击、设备报警、状态变更、阶段性结果等;
-
命令下发与状态回传:模块之间发指令、报状态、传结果;
-
模块之间的解耦通信:不想让模块互相持有引用、又要能通信时;
-
高频、定时产生的数据流:由于底层直接基于 LabVIEW 原生队列,且消息值改用 Variant 传递(避免了平化字符串的序列化开销),AMCLite 完全有能力扛住连续采集、实时数据这类高速场景,可以直接用它在采集循环和处理/存盘循环之间搬运数据。
一句话:从小工具到大型多循环系统,从低频事件到高速数据流,AMCLite 都能兼容着用。 这也是我这些年一直没换框架的原因——不用为不同项目在不同框架之间反复切换。
二、安装
AMCLite 以 VIPM 包(.vip 文件)形式分发,安装非常简单。

图1:下载得到的 VIPM 安装包
下载后得到如上图所示的文件,双击即可安装(前提是已安装 VI Package Manager)。按提示一路 Next 即可完成。
安装完成后,库文件位于:
National Instruments\LabVIEW 2023\vi.lib\AMCLite
路径中的
LabVIEW 2023会随你实际安装的 LabVIEW 版本变化。
安装完成后重启 LabVIEW,在程序框图的函数选板中,依次展开 Addons ---> AMCLite,即可找到整个工具包:

图2:AMCLite 函数选板位置
选板中提供了两类内容:常规使用的功能 VI 和 可直接拖入使用的模板 VI,下面分别展开介绍。
三、核心概念:Local 与 Global
在讲具体 VI 之前,必须先讲清楚 AMCLite 最重要的一个设计——消息发送分为 Local 和 Global 两套。理解了这一点,整个框架就通了。
Local:靠队列引用寻址
Local 系列 面向的是"我手上已经有这个队列的引用"的场景。
典型场景就是经典的 QMH:在同一个 VI 里,左边的消息产生器循环(UI 事件结构)和右边的消息处理器循环,共享同一个队列引用。产生器把消息塞进队列,处理器取出来处理。
这种方式的特点是快、直接、不会出错——引用是编译期就连好线的,不存在名字写错找不到队列的问题。
Global:靠队列名寻址
Global 系列 是 AMCLite 相对原生 AMC 最重要的增强。
它的思路很简单:每个消息队列在创建时都有一个唯一的名字,发送方只要知道这个名字,就能给它发消息,完全不需要拿到队列引用。
这带来的好处是巨大的:
-
跨 VI 通信零成本。子 VI 想给主界面发个状态更新?直接
Send Global Message,队列名填"UI"就行。不用把队列引用作为参数一层层往下传,也不用搞全局变量存引用。 -
模块彻底解耦。发送方只需要知道"收件人叫什么名字",完全不需要知道对方是谁、在哪、怎么创建的。新增一个模块不需要改动任何已有代码的接线。
-
动态调用友好。异步动态调用起来的 VI,天然拿不到主 VI 的引用,用 Global 方式就毫无障碍。
在实际项目里,我的习惯是给每个功能模块的队列起一个语义清晰的名字,比如 "UI"、"DAQ"、"Logger"、"Instrument"。整个系统的通信关系一目了然,看名字就知道消息发给谁。
四、功能 VI 详解
选板中提供了五个常规 VI:Send Local Message、Send Local Messages、Send Global Message、Send Global Messages、Flush Message Queue。
4.1 Send Local Message

图3:Send Local Message
向指定队列引用发送单条消息。
主要输入参数:
-
Message Queue:目标消息队列的引用;
-
Message:消息名(字符串),用来标识这条消息的类型,通常直接作为消息处理器中 Case 结构的选择器;
-
Message Value(Variant):这里就是 AMCLite 的核心改动。消息值类型为 Variant,你可以把任意 LabVIEW 数据类型直接连上去——数值、数组、簇、波形、路径、甚至引用句柄,不需要任何转换;
-
Priority Message:优先级标志。默认为 False,消息追加到队列末尾,按 FIFO 顺序处理;设为 True 时,消息会插入到队列最前端,下一次读取时被优先处理。
关于 Priority Message,最典型的用法是错误处理。当某个模块检测到严重错误,需要立刻中止当前流程时,就用优先级消息插队,避免错误消息排在一堆待处理消息后面迟迟得不到响应。
Variant 带来的实际差别有多大? 举个例子,假设你要把一个采集到的波形数组连同配置簇一起发给记录模块:
-
原生 AMC 的写法:先把数据
Flatten To String,接收端再Unflatten From String,还得小心两端的类型定义完全一致,改一个字段就得两边同步改,出错了还没有明确报错,只会得到一堆乱码或者莫名其妙的默认值; -
AMCLite 的写法:数据直接连到 Message Value 端子,接收端一个
Variant To Data搞定,类型不匹配时会直接报错,问题一眼看得见。
代码量少了,出错概率也小了。
4.2 Send Local Messages

图4:Send Local Messages
向指定队列引用一次性发送多条消息。
功能与 Send Local Message 一致,区别是消息以数组形式批量传入,会按数组顺序依次入队。
这个 VI 在初始化和流程编排时特别好用。比如一个测试流程需要依次执行"上电 → 自检 → 加载配置 → 开始测试",直接用一个数组常量一次性把四条消息压进队列,消息处理器会按顺序逐条执行,流程逻辑清晰可读,比在多个 Case 里互相跳转要好维护得多。
4.3 Send Global Message

图5:Send Global Message
按队列名发送单条消息。
与 Local 版本的唯一区别:不再输入队列引用,而是输入目标队列的名称字符串。VI 内部会根据名字查找对应的队列并投递消息。
这是 AMCLite 用得最多的一个 VI。任意一个 VI、任意一个循环、甚至一个被动态调用起来的子 VI,只要知道目标队列名,一行代码就能把消息送到。
举几个我在项目里的实际用法:
-
子 VI 向主界面回报进度:一个耗时的算法子 VI,在计算过程中不断
Send Global Message到"UI"队列,消息名"Update Progress",值为进度百分比。主界面收到就刷新进度条。子 VI 完全不需要任何界面引用; -
异常统一上报:所有模块遇到错误,一律
Send Global Message到"Main"队列,消息名"Error",值为错误簇。主控循环集中处理,要弹窗、要写日志、要安全停机,全在一处决定; -
跨模块联动:仪器通信模块拿到测量结果,直接发给
"Logger"存盘、发给"UI"显示,两个接收方互不知情,各改各的。
注意:Global 发送依赖目标队列已经创建。如果队列名对应的队列还不存在(比如目标模块尚未启动,或者已经退出销毁了队列),发送会失败。在实际工程中,建议在系统启动流程里明确各模块队列的创建顺序,或者对发送结果做必要的错误处理。
4.4 Send Global Messages

图6:Send Global Messages
按队列名一次性发送多条消息。
Send Global Message 的批量版本,消息以数组形式传入并按序入队。用法参照 4.2 节,不再赘述。
4.5 Flush Message Queue

图7:Flush Message Queue
清空指定消息队列中所有待处理的消息。
它会一次性移除队列里所有还没被读取的消息,而不需要逐条读出来丢掉。
典型使用场景:
-
中止操作:用户点了"停止"按钮,此时队列里可能还积压着一堆待执行的测试步骤消息。先 Flush 清空队列,再压入
"Stop"消息,才能真正做到立即停止,而不是把剩下的步骤全跑完再停; -
状态切换:从"自动模式"切到"手动模式"时,清掉自动模式遗留的待处理指令,避免模式混乱;
-
异常恢复:出错后重置流程,把脏数据清干净再重新开始。
Flush + 优先级消息是一对黄金搭档:先清空、再插队,能保证关键指令得到最快响应。
五、模板 VI
除了功能 VI,AMCLite 还内置了三个开箱即用的模板:Single Loop SM、Double Loop SM、Pure Message SM。

图8:AMCLite 模板选板
这三个模板可以直接从选板拖入空白 VI 的程序框图,自动生成完整的骨架代码——队列创建、消息读取循环、Case 结构、退出处理、队列销毁全都写好了,你只需要往里填业务逻辑。
这是我认为 AMCLite 最省时间的部分。 新开一个 VI,拖一个模板进来,五秒钟就有了一个结构规范、可扩展、能正确退出的程序骨架。团队里所有人写出来的代码结构都一致,接手别人的项目也不用重新理解架构。
5.1 Single Loop SM(单循环状态机)

图9:Single Loop SM 模板
单循环架构:一个 While 循环 + 一个 Case 结构,事件结构内嵌在其中——熟悉 JKI State Machine 的朋友对这个结构会很有亲切感。
它把"消息驱动"和"事件响应"合并进了同一个循环,运行逻辑是这样的:
-
循环从队列读取消息,消息名作为 Case 选择器,进入对应分支执行;
-
其中有一个专门的 "Idle"(空闲)分支,里面放着事件结构。当队列里没有待处理消息、循环进入空闲状态时,就停在事件结构上等待用户操作;
-
一旦捕获到前面板事件(按钮点击、控件值改变等),就把对应的消息压入队列,循环随即取出并跳转到相应分支执行;
-
Case 分支内部同样可以继续往队列里压消息,驱动状态一步步流转。
这样一来,用一个循环就同时搞定了 UI 事件响应和状态流转,不需要开第二个循环,结构比双循环更紧凑、数据在循环内直接用移位寄存器传递、也没有两个循环之间同步退出的握手负担。
适用场景:
-
中小型带界面的 VI,UI 逻辑不算特别复杂
-
流程/状态明确、以顺序流转为主的应用
-
想要单循环、结构紧凑、维护简单的场合
-
熟悉并偏好 JKI 风格状态机的开发者
一个需要留意的点:因为事件响应和业务执行在同一个循环里,如果某个 Case 分支执行时间很长,循环就无法回到 Idle 分支的事件结构,这段时间内前面板的操作不会被立即响应。若你的应用里存在耗时较长、又不能阻塞界面的操作,就应该改用下一节的 Double Loop SM,把界面和业务彻底拆到两个循环里。
5.2 Double Loop SM(双循环状态机)

图10:Double Loop SM 模板
经典的 QMH 生产者-消费者结构:左边消息产生器循环(Event Structure),右边消息处理器循环(While + Case)。
-
左侧循环:事件结构捕获前面板的用户操作(按钮点击、控件值改变、面板关闭等),把每个操作打包成消息投入队列;
-
右侧循环:从队列取消息,在 Case 结构中执行对应的业务逻辑。
两个循环并行独立运行,这样做的关键价值在于:界面响应和业务执行彻底分离。即使某个业务操作耗时几秒钟,UI 循环依然在正常响应用户点击,界面不会卡死。
适用场景:
-
需要在响应界面的同时执行耗时较长后台任务的 VI
-
UI 交互复杂、业务逻辑较重的应用
-
中大型应用的主程序面板
它和 5.1 Single Loop 的分工可以这样把握:界面简单、没有长耗时阻塞操作时,用 Single Loop 更紧凑;一旦某些操作耗时较长、又必须保证界面随时可响应,就用 Double Loop 把界面和业务彻底拆到两个循环里。实际项目中,稍具规模的主面板我基本都用它。
关于两个循环的退出:模板中已经处理好了。用户点击退出时,UI 循环发送退出消息给处理器循环,同时自身条件端子置 True 退出;处理器循环收到退出消息后,执行清理动作、销毁队列,然后退出。这个握手过程是 QMH 里最容易写错的地方,模板帮你写好了,直接用就行。
5.3 Pure Message SM(纯消息状态机)

图11:Pure Message SM 模板
去掉界面元素,只保留纯粹的消息驱动结构。
它不包含 UI 事件处理,所有的驱动力都来自外部通过 Send Global Message 发进来的消息。VI 启动后创建自己的命名队列,然后就安静地待在那里等消息,收到什么执行什么。
适用场景:
-
通过异步动态调用启动的后台服务型模块
-
数据记录服务、日志服务
-
仪器通信代理
-
任何"常驻后台、被别人调用"的功能模块
这个模板和 Global 消息机制是天生一对:把一个 Pure Message SM 动态调用起来跑在后台,系统里任何地方需要它干活,直接按队列名发消息就行。这基本上就是用 LabVIEW 实现的一个轻量级"服务"。
在稍大一点的项目里,我通常会这样组织:
-
主界面用 Double Loop SM
-
每个后台功能模块(采集、存盘、通信、报警)各用一个 Pure Message SM,异步动态调用启动
-
模块之间全部通过 Send Global Message 按名字通信
整个系统的结构非常清晰:一个主界面 + N 个后台服务,靠消息串起来。加模块只需要新增一个 VI 和一个队列名,不用动任何已有代码。
六、使用范例
AMCLite 附带了一个可直接运行的示例,位于选板的 Example 子面板中,名为 Queued Message Handler Fundamentals:

图12:Example 示例
示例代码如下:

图13:示例代码
建议直接打开这个范例跑一遍,配合断点和高亮执行,观察消息是如何从产生器进入队列、再被处理器逐条取出处理的。看十遍文档不如自己跑一遍代码。
七、实战建议
最后分享几条这些年用下来总结的经验,都是踩过坑之后留下的。
1. 消息名直接用字符串即可
消息名和队列名直接用字符串就好,简单直观,不建议特意套一层 TypeDef 枚举或者常量子 VI——那样反而增加维护成本,也失去了字符串消息"随手加一条新消息"的灵活性。
唯一要提醒的是:字符串是手打的,注意别拼错。消息名或队列名一旦写错,消息可能悄无声息地进了错误的分支、或者发到一个不存在的队列,不会有明显报错。所以敲字符串时留个心,两端对照着写,出问题时也优先检查一下是不是拼写不一致。
2. Variant 取值时做好类型校验
Variant 灵活是灵活,但也把类型检查从编译期推到了运行期。Variant To Data 的错误输出不要直接丢弃,接上错误处理,这样类型不匹配的时候能立刻定位到是哪条消息出了问题。
一个更稳妥的做法:对于结构复杂、字段多的消息,用 TypeDef 簇作为消息值的类型,两端都引用同一个 TypeDef。这样改字段的时候两端自动同步,不会出现单边遗漏。
3. 队列的创建与销毁要配对
每个线程创建的队列,退出时必须销毁。忘记销毁会导致队列引用泄漏,长时间运行的程序上这个问题会逐渐累积。模板里已经处理好了配对逻辑,不要随意删掉模板里的销毁代码。
同时也要注意销毁的顺序:如果 A 模块还在给 B 模块发消息,就不能先把 B 的队列销毁掉。退出流程建议由主控模块统一编排,先通知所有子模块退出,等它们全部确认退出后,主控再退出。
4. 善用优先级消息,但别滥用
优先级消息插队处理,适合错误、急停、中止这类必须立刻响应的场景。但如果什么消息都设优先级,队列的 FIFO 语义就被破坏了,执行顺序会变得不可预测,反而更难调试。优先级消息应该是少数派。
八、总结
AMCLite 做的事情其实不复杂,就是在原生 AMC 的基础上做了三件事:
-
消息值改为 Variant —— 任意数据类型直接传,告别 Flatten/Unflatten 的胶水代码;
-
移除 UDP 网络通信 —— 砍掉单机项目用不到的部分,框架回归轻量;
-
新增按队列名寻址的 Global 消息 —— 跨 VI 通信零成本,模块彻底解耦。
再加上三个开箱即用的模板,让"新建一个结构规范的 VI"这件事变成五秒钟的事。
这套东西谈不上多高深,但胜在足够简单、足够顺手、足够稳定。小项目用它不觉得重,大项目用它不觉得不够,从几年前的老项目到现在的新项目一路兼容下来,没有为了框架本身返工过。
对于日常做 LabVIEW 开发的工程师来说,有一套自己用得顺手、且能长期沿用的架构底座,价值可能比学十个新特性都要大。
如果这篇文章对你有帮助,欢迎点赞收藏。有任何问题或者建议,也欢迎在评论区交流。
博主主页简介提供AMCLite工具包下载。
版权与免责声明
AMCLite 基于 NI 开源的 AMC 参考库修改而来,原库版权归 National Instruments 所有,依据 Apache License 2.0 授权,安装包内已随附许可证与 NOTICE 文件。
AMCLite 为独立第三方修改版本,与 NI 无隶属或背书关系。

352

被折叠的 条评论
为什么被折叠?



