很多人图省事,在并行循环之间用「局部变量」或「全局变量」传数据:循环 A 写、循环 B 读,线一拖完事。程序大部分时间"看起来正常",可一到现场就偶发丢数、读数乱跳、状态莫名其妙——查半天查不出原因。这多半不是玄学,而是竞争条件(race condition):两段代码在抢同一块内存,谁先谁后全看运气。
一、先说清楚:什么是竞争条件
在 LabVIEW 里,Local Variable(局部变量)和Global Variable(全局变量)读写的是同一块内存地址,并且本身不带任何锁。当两个以上并行的循环 / VI 同时访问它时,读写顺序就脱离了你的控制:
[循环A] x = 读取共享变量 // 读到一半 [循环B] x = 写入共享变量 // 把值改了 [循环A] x = x + 1 // 基于"旧值"算,结果错
更糟的是,这种错误不可复现:单步跑、低负载时一切正常,高负载 / 多任务抢资源时才偶尔冒头,现场极难复现。这也是它最危险的地方。
图形化编程把"共享变量"做成一个不起眼的小方框,拖起来零成本,于是很多人不知不觉把它当成了跨循环传数据的默认手段。但越是顺手的捷径,越容易埋雷。
局部变量/全局变量造成竞争条件,四种同步机制是它们的确定性替代
二、队列 Queue:有缓冲的数据通道
- 核心节点:
Obtain Queue→Enqueue Element/Dequeue Element→Release Queue。 - 队列是先进先出(FIFO)的缓冲区,元素入队一份、出队就少一份。
- 关键特性:有缓冲。生产者循环快、消费者循环慢时,数据先堆在队列里,既不会丢,也不会把生产者堵死。
- 典型用法:DAQ 采集循环把采样数据 Enqueue,分析 / 显示循环 Dequeue,两个循环彻底解耦,这就是经典的生产者—消费者模式。
- 注意:队列里每个元素都是独立拷贝,和"通知只留最新值"完全不同,别混用。
三、通知 Notifier:只保留最新值的广播
- 核心节点:
Obtain Notifier→Send Notification/Wait on Notification。 - 通知是一对多广播:发送方 Send 一个值,所有正在 Wait 的循环被同时唤醒。
- 它只保存最新值:连续 Send 三次才被 Wait 到,前两次直接被覆盖("最后写赢")。
- 典型用法:UI 线程把"停止 / 暂停 / 切换模式"下发下去;或在多个循环间广播"当前状态"。中间过程不重要,大家只关心最新状态。
- 提醒:
Wait on Notification一定要设超时,否则某个循环可能永远卡在等待上。
四、信号量 Semaphore:限流与互斥锁
- 核心节点:
Acquire Semaphore/Release Semaphore。 - 信号量是一个计数器,用来控制"同时能有几个任务访问某资源"。
- 限流:初始化为 N(如 3),每进来一个任务 Acquire(计数 -1),出去 Release(计数 +1);计数到 0 时后续任务排队等待。
- 互斥锁:初始化为 1,谁 Acquire 到谁独占(二值信号量),适合保护串口、仪器句柄这类稀缺资源。
- ⚠️ 铁律:Acquire 之后必须 Release,且成对出现。忘了 Release 就会死锁——下一个任务永远拿不到,程序卡死。建议把两者写在同一个顺序结构里。
五、会合 Rendezvous:等所有任务到齐
- 核心节点:
Rendezvous(创建会合点)→Wait at Rendezvous。 - 会合是同步屏障(barrier):你在 N 个并行任务里都放一个
Wait at Rendezvous,只有当全部 N 个任务都到达这一行,才一起放行。 - 典型用法:多分支并行计算,等各分支都算完再汇总;或多个设备要求"同时起跑"。
- 注意:会合要求参与方数量固定,少一个任务到不了,所有任务都会卡在会合点。设计时要确认每个分支都能真正执行到 Wait。
六、一张表选对机制
| 机制 | 核心特性 | 适用场景 | 一句话选型 |
|---|---|---|---|
| 队列 Queue | 有缓冲、FIFO、取走即少 | 数据流传输、生产者—消费者 | 要"传一串数据、不能丢" |
| 通知 Notifier | 只留最新值、一对多广播 | 状态 / 命令下发 | 要"通知大家一个当前状态" |
| 信号量 Semaphore | 计数限流、可当互斥锁 | 限制并行数、保护稀缺资源 | 要"限制几个任务同时干" |
| 会合 Rendezvous | N 点汇合、屏障同步 | 多任务同步起跑 / 汇总 | 要"所有分支到齐再继续" |
七、实战:生产者—消费者 + 停止通知
最稳的并行框架长这样:采集循环用队列喂数据,UI 循环用通知发停止信号,两个工作循环各自等待停止通知后优雅退出。数据共享走队列,控制信号走通知,全程不碰共享变量。
[采集循环] sample = DAQ.Read() Queue.Enqueue(sample) // 数据走队列 if Notifier.Wait(超时=100ms) == 停止: break [处理循环] sample = Queue.Dequeue() Analyze(sample) if Notifier.Wait(超时=100ms) == 停止: break [UI 循环] if 按钮"停止".Value: Notifier.Send(停止)
要点:数据共享用队列,控制信号用通知,二者都不依赖局部 / 全局变量,竞争条件从源头消失。信号量 / 会合在需要限流或同步汇合时再叠加使用。
八、小结
并行编程难的不是"跑起来",而是"每次都跑对"。局部变量和全局变量给你便利,代价是不确定的时序;四种同步机制给你确定性,代价只是多连几根线。下次在并行循环间想拖一个局部变量时,先想一秒:这里到底该用队列、通知、信号量,还是会合?