写好LabVIEW程序不可不知的利器(四):Event Producer/Consumer
写好 LabVIEW 程序的利器(四): Event Producer/Consumer
前一篇文章谈到的是如何利用 Functional Global Variable 来将程序功能模块化,在这种概念下,写程序就不再是边写边改,而是一开始就可以先规划好程序里面需要用到哪些功能,接着才将这些模块一个一个写出来。
同样以红绿灯的程序为例,首先需要一个红绿灯的模块,它必须要有初始化、更改亮起的灯号,以及读出目前红绿灯状态的功能。如上图, State 用来设定模块要执行哪一个程序,当执行 Write 时,输入的 Light Command 可以指定要写入的灯号;而当执行 Read 时,则会将目前的红绿灯状态由 Traffic Light 输出。以下说明模块内部的程序码。
(1) Initial : 将初始值写入 Shift Register 。
(2) Write : 将指定的灯号设为 true ,并且写入 Shift Register 。
(3) Read : 将目前 Shift Register 的值读取出来显示。
其中可以注意到的是,我在这个模块里面加了 error in 和 error out 的接点。当有 error 发生时,里面的程序就不去执行,如下图所示。即使程序用不到这个机制,也可以藉由资料流来控管各个模块在主程序里面执行的先后顺序。
另外,程序里面还需要一个计时器的功能,它要可以去设定时间,并且等待到时间到达后,才继续去做下一件事。如上图,这个模块和秒表的功能相同,可以 Reset 将要等待的秒数写入,然后每隔一段时间去 Check 时间是否到达。以下是这个模块的程序说明。
(1) Reset : 将 Wait Time 以及目前起始的时间写入 Shift Register 。
(2) Check : 检查目前经过的时间是否到达设定的 Wait Time 。
当写好所有需要用到的程序功能的模块后,接下来就是选择一个 Design Pattern 来当作主程序的架构。其中最常被用到的是 State Machine ,初学者也较容易上手。但 State Machine 的架构下,即使不做事在 Idle 的状态下,程序也会不停的 Polling ( 轮询 ) 去确认按键有没有被触发来改变状态,资源被平白浪费掉了。
而比较好的写法是 Event Producer/Consumer 的架构,如下图。在这种程序架构下,只有当使用者去触发按键时, Producer 才会将指令排入 Queue 里面,接着 Consumer 才会去执行任务,而平时两个循环并不会执行程序。接下来教大家如何把前面写好的模块加入 Event Producer/Consumer 的架构。
首先在人机界面上,要可以提供使用者自行输入灯号的顺序,以及停留的时间,如下图。
接下来就要把要亮起的灯号,以及等待时间写入 Traffic Light Module 和 Timing Module 中,但很明显的这两者的资料型态不一样。解决的方法是在一开始定义 Queue Element 的资料型态中,将两个不同的资料型态和 State 用 Cluster 包起来传递,如下图。
不过这种作法很容易当程序里面有一大堆不同的资料形态要进行传递时, Cluster 会越来越大而难以管理。但其实除了 State 之外,传递资料时同时只会有一种资料型态输入至模块,所以更好的做法是使用 Variant 来传递资料。当 Producer 下了一道指令 ( State ) 和资料 ( Data ) 时,都将资料转成 Variant ,而 Consumer 收到指令后再将其转回特定的资料型态。如下图,将 Queue State ( Enum Control ) 和 Data ( Variant ) 用 Cluster 包起来做为 Queue element 的资料型态。
在 Producer Loop 里面的 Event Structure 设定当使用者按下 Start 按键即触发,接着会把使用者输入的灯号顺序 Commands 用 For Loop 将 Array 拆开,然后再用 Unbundle By Name 解出要亮起的灯号 ( Light ) 以及停留时间 ( Wait ),分别用 To Variant 包进 Queue Command 里面再 Enqueue 排入序列当中。所以这边做的事就是下指令给 Consumer ,让它先去执行 Write 改变灯号,然后再 Reset 计时器并写入 Wait Time ,让它等待时间到达后再去做下一件事。
在 Consumer Loop 中,当它收到指令后,会先用 Unbundle By Name 将 Element 解开,然后根据里面的 State 来执行指定的任务。例如前面 Producer 先下了一个 Write 的指令, Consumer 在这个状态下会先用 Variant to Data 转回原本的资料型态,然后再写入 Traffic Light Module 更改亮起的灯号。接着要去更新人机界面上的红绿灯,所以后面再插队排入 Display 的指令。虽然 Queue 里面还有 Reset 的指令,但它就会优先执行完 Display 的指令再去处理后续的任务。
在 Display 的指令下, Consumer 会去读取 Traffic Light Module ,然后更新人机界面上的红绿灯。
在 Reset 的指令下, Consumer 同样要先将 Data 用 Variant to Data 转回原来的资料型态,然后再将 Wait Time 写入 Timing Module 中。接下来因为要去检查是否已经到达等待时间了,所以同样以插队的方式排入一个 Wait 的指令,让 Consumer 预先去执行。
在 Wait 的指令下, Consumer 会去检查 Timing Module 是否已经到达等待时间?若还没到达,则预先排入 Wait 的指令,让它继续去检查;若已经到达等待时间的话,就预先排入一个 Initial 的指令,让灯号初始化。
Initial 的指令会初始化红绿灯,将灯号全部先关起来,然后同样去更新人机界面的红绿灯。通常在程序一开始,会先做初始化的动作,而在这个架构下, Initial 变成一个指令。除了一开始就可以先 Enqueue 进去一笔 Initial 的指令之外,在程序执行中随时都可以去下 Initial 的指令来做初始化。
程序停止也是由 Producer 来控制,新增一个 Stop 按键触发的 Event ,当使用者按下 Stop 时, Producer 就会送出 Stop 的指令。
当 Consumer 收到 Stop 的指令时,就将 Queue 里面的资料全部清空,然后停止循环。
最后完成的程序如下,大家可以发现里面已经看不到程序码了,只有 Event Producer/Consumer 的架构,以及去设定写好的 Module 。程序码以及 Shift Register 都被藏在模块里面,需要新增或是修改程序功能时,不用去动到主程序架构,只要去修改模块里面的程序码即可。这样的程序写法也比较直觉,而且写完之后也比较好去维护,程序也比较不易越写越大而复杂。
转载
谢谢分享,不错的资料
按照你的表述,我在最后这个生产者消费者模式中写不下来你的程序,你可不可以把源代码发一份给我啊 ,多谢了 601924437@qq.com
感谢分享
很好的资料,谢谢分享
很好的资料,谢谢分享
多谢分享!
谢谢分享
生产者消费者模式.谢谢
不错的资料,谢谢分享
不错的资料~~~~~~~~~~学习了~