全局变量和功能全局变量
全局变量
全局变量与局部变量类似,是一种可以让数据随时流入流出其间的对象。它与局部变量的区别在于,局部变量只能在其对应控件所在的 VI 中使用,而全局变量可以在一个程序中的任意 VI 上使用。使用 全局变量时,数据被保存在某一固定的内存空间内,不随数据线流动,在需要读写数据的地方,直接对全局变量进行操作即可。
LabVIEW 的全局变量和局部变量是非常容易被滥用的功能,因为它们用起来很方便,可以随时随地读写数据。但是它们非常不安全,容易导致一些莫名其妙又难以调试的错误,同时它们还破坏了数据流顺序的逻辑关系,导致程序可读性和可维护性严重下降。所以我们应当尽量避免在程序中使用全局变量或局部变量。我们在局部变量一节讨论了适合使用局部变量的场景。对于全局变量,笔者建议仅仅把它用于常量定义,其它情况都应该避免使用。下面我们详细讨论一下全局变量的用法和问题。
创建全局变量
在 LabVIEW 的新建对话框中选择“全局变量”即可创建出一个空白的全局变量 VI。

或者在一个 VI 的程序框图上,选择函数选板的“编程 -> 结构 -> 全局变量”,也可以创建出一个空白的全局变量实例,双击这个全局变量实例,会打开一个空白的全局变量 VI。全局变量 VI 是一种特殊的 VI,它只有前面板,没有程序框图。在全局变量 VI 的前面板上可以放置多个控件,每个控件表示一个全局变量数据。由于一个全局变量 VI 可以存放多个全局变量数据,在编程时,可以将相关度较大 的全局数据都放在一个全局变量 VI 中,以便于管理。

不论控件是控制型的还是显示型的,它所表示的全局变量都既可读也可写。在 VI 上任何需要使用这个全局变量数据的地方,把全局变量 VI 拖到程序框图上即可生成全局变量实例,然后对其进行读写。下图中内含小地球图标的矩形方块就是一个全局变量实例,在同一版本 LabVIEW 的程序框图上,全局变量与局部变量的外观是一致的,只有图标有所区别,一个是小地球,一个是小房子。

通过这个全局变量实例,程序可以读写全局变量中的数据。如果一个全局变量 VI 上有多份全局变量数据(多个控件),在这个全局变量 VI 对应的实例上点击鼠标左键,或在右键菜单的“选择项”中,可以把这个实例与全局变量中不同的数据相关联。
通常,在不会引起误解的情况下,我们会把全局变量 VI、全局变量数据、全局变量实例,都简称为全局变量。
数据竞争状态
竞争状态是指在多个线程中,由于同时访问同一资源所引发的程序结果的不确定的状况。比如下面这个程序,假设全局变量 Data 的值原本为 0,运行完下面这个 VI,Data 中的值是几呢?

从程序的逻辑上看,它先把 Data 加 2 再减 1,程序设计的意图是希望结果为 1。但实际上,运行 VI 后,Data 的值可能是 1,也有可能是 2,还可能是 -1。程序中的代码分为上下两个部分,两部分之间没有数据线相连。按照我们前面所介绍的 LabVIEW 的运行机制,这两部分的代码应当同时在不同的线程内运行。
这里需要更确切地解释一下:所谓的“同时运行”在计算机科学中被称为并发(Concurrency)。在多核 CPU 系统中,线程确实可能在物理上同时执行(并行);而在单核或核心数受限的情况下,操作系统会快速地在不同线程间切换。这种切换发生的时间点是随机且极快的(通常是纳秒或微秒级)。
由于我们无法预知操作系统何时挂起一个线程并运行另一个线程,这就导致了代码执行顺序的不确定性。
我们从微观时间的角度来考虑一下上图中的程序:程序没有指定上下两个部分的代码的执行次序,程序先执行哪一个任务是随机的。若程序先执行完上半部分的全部代码,再执行下半部分代码,则 Data 最终的值是 1。若程序先执行上半部分的读全局变量,再加 2;然后切换到另一线程,执行下半部分的读全局变量,再减 1;接着执行上半部分的写全局变量;最后执行下半部分的写全局变量。Data 最终的值就是 -1。所以,这个程序的运行结果是不确定的,也就是说,出现了竞争状态。
局部变量同样会引起竞争状态,比如下图中的程序:

这个程序中添加了两个延时子 VI“Stall Data Flow.vim”,它们会让程序 延时一毫秒。这个延时是为了提高竞争状态出现的概率,以方便我们解释问题。在程序同样假设局部变量 Numeric 的初始值是 0。
- 如果程序执行的顺序是,先在 A 处取出数据 0,加 1,再在 B 处写入数据,这时局部变量数值为 1,再从 C 处取出数据,还是 1,减 1,在 D 处写入结果。最终局部变量的结果是 0。
- 如果程序先在 A 处取出数据 0,加 1,然后从 C 处取出数据,这时局部变量中的数据仍然是 0,之后在 B 处写入数据 1,之后再把从 C 处读取的 0 减 1,在 D 处写入结果。最终局部变量的结果是 -1。
- 如果程序先在 A 处取出数据 0,加 1,然后从 C 处取出数据,然后减 1,在 D 处写入结果 -1,之后在 B 处写入数据 1,这样程序的最终局部变量的结果是 +1。
我们肯定不会希望自己的项目里有这种结果不确定的代码出现。相比之下,全局变量比局部变量更糟糕,因为局部变量只在一个 VI 内有效,我们如果认真检查一个 VI,也许还可以排查出有竞争状态的代码。但全局变量在所有的 VI 内都可以被读写,排查问题的难度就被大大增加了。尤其是在有多人合作的大型项目中,我们可能很难确保一个全局变量有没有被别人在其它某个 VI 里改动了。
除了可能出现的竞争状态,全局变量会降低代码的可读性,因为代码上的全局变量,不能直观地反映出它的数据来源。数据线和数据流动方向是理解 LabVIEW 代码时重要的提示信息。此外,VI 每次读取全局变量数据,LabVIEW 都要为读到的数据复制一个新的副本,这导致它的运行效率也比较低。鉴于这么多的缺点,我们应该尽量避免使用全局变量。