跳到主要内容

VI 中的数据空间

· 阅读需 6 分钟

    LabVIEW 由于比其它语言采用了更多的值传递方式,这必然会影响它的运行效率,也使得 LabVIEW 在这方面要采取一些其它语言不需要的应对措施,尽量提高效率。优化之一是子 VI 中局部变量使用的内存的分配方式。
    C 语言中,函数的局部变量存在于栈中。在调用某一函数时,程序才为这个子函数开辟一块空间作为用于保存函数中局部变量的栈。子函数运行结束后,栈空间即被释放。下次再调用这个函数,程序会重新非配栈空间,这时的空间可能与上次分配的并不在同一内存地址。为了节约反复开辟空间的时间,LabVIEW VI 中并没有采用栈的方式。一般情况下,静态调用 VI,每个 VI 专门有一块存数据的数据空间,这块数据空间所在的内存地址在 VI 每次运行时是不会变化的,尤其是上次 VI 运行后所留有的数据还可以被使用。

    LabVIEW 这种做法最大的好处是节约了大量开辟、回收内存的开销;但它也有个严重的缺陷,这也是其他语言不采用类似措施的原因:每次函数调用没有独立的数据区,因此无法实现递归调用(LabVIEW 静态调用的情况下)。经过权衡,LabVIEW 最终牺牲了递归来换取运行效率。

    对于一般的子 VI(非可重入的),不论在程序的哪里被调用时,都使用的是同一块数据区。如果主 VI 上有两个并排被调用的同一个子 VI(如图1所示的两个 Delay VI),理论上的数据流驱动语言是应该在两个线程内同时运行两份子 VI 的代码。但是,由于这两次调用会使用到同一块数据区,为了避免两次运行之间互相干扰,引起数据混乱,LabVIEW 实际上是顺序执行这两次调用的。至于那部分代码被先调用是不确定的。


图1:并行调用同一子 VI 两次

    LabVIEW 只能顺序执行这两次调用,在很多时候并不是一件坏事。比如,子 VI 中的操作是读写某一串口。LabVIEW 的这一特性恰好防止了多线程同时对这个串口读写而引发的错误。但这种行为也会引起一些糟糕的问题。比如,子 VI 是用来读写所有串口的。我在一个线程内对串口1做了操作,另一个线程要对串口2操作。读写串口是比较慢的,本来应该两个串口同时操作,来节约一点时间。但是如果串口读写子VI不能重入,那其中一个线程就只好慢慢等着了。
    LabVIEW 解决这个问题的办法是为 VI 增加了一个可重入(reentrant)属性。非可重入的 VI 的数据区是和这个 VI 其它内容(比如执行代码、界面、源代码等)放在一起的,所以不论这个 VI 在哪被调用,使用的都是同一数据区。设置为可重入的 VI,它的数据区被开辟在调用它的父 VI 那里。在父 VI 的程序框图上每一个可重入子 VI 的图标,都意味着父 VI 的空间内为这个 VI 开辟了一块数据区。所以,并行的两次调用同一可重入子 VI,这两次调用它们使用的是不同的数据区,所以可以同时运行而不需要担心数据被互相干扰;如果是循环内有一个子VI,那么循环多次执行,每次调用这个子 VI,使用的还是同样的数据区。