




已阅读5页,还剩75页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
reference 死锁问题LabVIEW中的引用经常需要和“In Place Element Structure”配合使用。In Place Element Structure 对一个引用的数据进行处理时,为了保证多线程安全,它会锁住引用指向的数据;其它线程若需对同一数据做操作,必须能这个In Place Element Structure中所有代码执行完毕才可,这样就避免了多线程读写同一内存数据所产生的竞争问题。举例来说,下面这段程序的执行时间是1秒:而下面这段程序的执行时间则是2秒:因为第二段程序中的两个In Place Element Structure必须顺序执行。有了“锁住”这个操作,就有不小心造成死锁的可能。比如对于同一数据的引用,千万不能嵌套使用In Place Element Structure,否则就会死锁:在上面这个示例中,程序运行至内层的In Place Element Structure,就会停在这里等外层In Place Element Structure运行结束,释放它锁住的数据;而对于外层In Place Element Structure来说,它内部的全部代码要运行结束,它才结束。因而它们相互等待,造成了死锁。Packed Project Libraries 2 与Library的比较acked Project Library 从名字上来看,就是被包装好了的Project Library。Project Library 是编程时候由程序员创建出来的。比如下图这个工程,我在里面创建了一个叫做“My Algorithm Library.lvlib”的工程库。它包含两个VI,其中一个是私有的。Packed Project Library 并不是手工创建的,他是通过一个项目的生成规范,从 Project Library 编译而来的。比如上图的项目,我创建了一个Packed Library类型的生成规范。我在这个生成规范中指定把“My Algorithm Library.lvlib”编译成Packed Project Library 。编译的结果是在我指定的路径下生成了一个名为“My Algorithm Library.lvlibp”的文件。它的后缀名仅比Packed Library多了一个字母p。双击这个文件,可以打开它,看到他里面包含的VI:如果需要在其它项目中使用到这个Packed Project Library,我们可以直接把它加到另一个项目中去,下图是一个演示项目:Packed Project Library 看上去和 Project Library 非常相似,用法也完全相同。Packed Project Library 与 Project Library 都是将功能相关的一组VI封装起来的方法; 库中的VI可以具有层次机构; 库中的VI都带有名字空间,名字空间是带有后缀名的库名; 都可以方便的放在项目管理器里使用尽管它们十分相似,Packed Project Library 与 Project Library 相比,还是有一些明显区别的: Packed Project Library 是通过编译生成的; Packed Project Library 中的VI是编译后产生的,它们不能被修改; Packed Project Library 包含有私有VI,但用户无法看到也不能使用它们; Packed Project Library 把VI,.lvlib以及其它用到的文件都打成一个压缩包,用户在磁盘上就只能看到一个.lvlibp文件,看不到VI文件; Packed Project Library 很适合作为最终产品发布给用户使用; 在项目中使用Packed Project Library 可以缩短编译时间,因为Packed Project Library 中的VI是已编译好的,不会再随项目编译一遍。(这一条先这样写上,但我还需要再深入研究一下)LabVIEW中LVClass数据转换成XML格式的问题前一段时间,一个同事的程序出了问题。他在程序中把一个LVClass类型的数据转换成XML格式,再保存成文件。但是从文件中把数据转回成 LVClass时,却出了问题:在调用“Unflatten XML”这个函数时,程序有时出错,有时又不出错。他的程序中使用了大量的LVClass,并且它们之间有着复杂的继承与包含关系,以至于花了两三天的事 件,才找出问题所在。其实是个简单的问题,只是在设计程序时他没有意识到。我做了一个简化的程序,可以重现这个问题:首先,给一个子类的对象设置一些数据。然后把它当做父类类型的数据,平化成XML文本,存盘:关闭LabVIEW,然后重新打开LabVIEW。再编写一个反向程序,把XML数据转换成父类类型的数据:发现Unflatten From XML函数返回一个错误,value中是一个空的数据。错误产生原因如下:在把子类数据转换成父类数据类型,这个类型虽然是父类的,但其数据仍然是子类的。再转换成XML格式,XML格式中记录的仍然是子类的数据。在反向过程中,Unflatten From XML拿到的数据是子类的,但它企图转换时,却发现内存中没有子类的类型信息,因此它也就不知道如何转换这个数据,所以报错。如果这个程序稍微改动一下,把XML数据直接转换成子类的数据,就不会出错了:实际上,子类的数据总是可以用父类来表示的。因此这个XML数据亦可以直接被转换成父类的类型,但前提是,一定要保证子类的类型别家在到内存中去了。只要在程序中放置一个子类的对象,自然就可以把子类加载至内存。像下面这个程序就可以正常工作:这个实验反映出两个问题:1. 把XML中的内容如果是属于某个LVClass类型的数据,把这些数据转换回LVClass数据时,那个LVClass一定要已经存在于内存才行。2. 在之前的一篇文章“LvClass 的一个效率问题”中提到过:当子类被加载如内存时,它所有的父类也会被加载入内存。但反过来并不成立。因为一个类有哪些父类是确定的,父类的地址就记录在子类中。但一个类并不知道他会有多少子类,任何人都可以从它派生出不同的子类来,因此它在装入内存时,不可能把自己的子类也都装进来。LabVIEW中实现链表、树等数据结构LabVIEW自带的数据结构只有数组和队列。多数情况下,这两种数据结构足够开发者使用了。但是,我平时使用C+和C#语言更多一些,所以编写程序时常常会想到使用其它编程语言中常见的数据结构比如链表(List)、树(Tree)等。LabVIEW中也可以编程实现这些数据结构,一个比较直观易懂的编程方法是基于LabVIEW中的类和引用来实现各类数据结构。我在我和 LabVIEW一书的第13.3.5节中介绍了一个简单的链表容器的实现方法,它是基于LvClass编写的,数据流驱动的一种容器。但是正如我在书中 提到的,它虽然和有一些和文本编程语言中的链表相类似的地方,但本质并不相同。文本编程语言中的链表,树等数据结构离不开引用(或指针),节点之间是通过 引用来相互关联的。LabVIEW可以为数据创建引用,因此也可以方便的实现与文本语言中功能相同的数据结构。这里插一段,介绍一下数据结构和数据容器的关系,我自己理解是这样的:数据结构侧重于数据的存储方式,比如如何排序;数据结构在加上与此结构相关的 操作方法,比如添加删除数据等方法,就构成了一个数据容器。脱离了操作方法,单纯的数据结构用处非常有限。因此,我文章中在提到数据结构或者数据容器时, 指的都是同一回事:数据结构和相关的方法。为了介绍如何在LabVIEW中实现一个数据结构,我打算以双向链表为例,讲解一下如何编写它。双向链表中每个节点都会记录上一个节点和下一个节点的位置。因此,在双向链表中,可以从一个节点直接跳转到它的上一个或下一个节点上去,也就是正向 或反向遍历整个链表。可以直观的想到,使用LvClass实现这样的节点,只要为这个节点创建一个类ListNode,并且这个类有两个成员变量,它们的 类型都是ListNode的引用,分别用于指向前一个和后一个节点就可以了:这样的设计在文本编程语言中是没有问题的,但在LabVIEW中行不通。其它编程语言中,程序运行时,才会对类的对象进行初始化。LabVIEW 中,VI一打开,它上面的控件和常量就需要被初始化了。某个对象在初始化时,它的成员变量也要被初始化,若它的成员变量的类型还是这个类,这以初始化的过 程就陷入了死锁:类需要它的成员变量先初始化;它成员变量需要这个类先初始化。基于同样的原因,一个类的成员变量的数据类型也不可以是这个类的子类:子类初始化需要先对它的父类进行初始化。但是,一个类的成员变量的数据类型可以是这个类的父类:父类在初始化的时候,不需要理会它的任何子类。既然父类初始化时,不依赖于子类的初始化;而子类的对象又可以被当做父类的类型来保存,咱们就可以利用这一特性在LabVIEW中实现可以数据结构 的节点了。只不过LabVIEW实现链表的节点要多一个步骤:我们需要为ListNode类再定义一个父类ListNodeVirtual。这个父类不做 任何实质性的工作,它仅用于保存相邻节点的引用。以上两个类是针对链表节点的双向链表本身也需要做成一个类:DoubleLinkedList类,这个类中封装有链表的属性和方法。比如它需要一个指向链表表头的引用,需要有为链表添加删除数据的方法,为遍历链表中的数据,还需要有一个迭代器作为演示,我只实现了链表的几个简单功能。演示程序工程结构如下:ListNode的成员变量包括一个数据,和两个指向前后节点的引用:DoubleLinkedList类的成员变量包括指向链表头节点的引用,迭代器指向的节点的引用,并记录了链表长度下面看一下链表中几个主要方法是如何实现的。首先是Append after Enumerator.vi这个方法,它是链表里最复杂的一个方法。它的输入是链表中一个新的节点,它把这个新节点添加在链表迭代器指向的那个节点的后面。在给链表添加数据时,会遇到两种情况。首先,这个链表是一个空链表,那么被添加的节点就是这个链表的首节点,链表的迭代器也应当指向这一唯一的节点。我设计的这个链表是一个环状链表。当链表中只有一个节点的时候,这个链表的上一个和下一个节点都是它自己。如果链表不是空的,就把新节点插在迭代器指向的节点的后面。因此:新节点的前一节点指向的应当是迭代器指向的那个节点;新节点的后一节点是迭代器指向节点原来的后一节点。迭代器指向节点的新的后一节点应当是这个新 节点;原来迭代器的后一节点的前一节点也应当换成这个新的节点。最后,我把迭代器也指向了这个新的节点,这样连续添加新节点时,它们会按照先后顺序插入链 表。我的演示程序还用到了其它几个方法。Reset Enumerator.vi 负责把迭代器复位,也就是指向链表的头节点:Enumerator go Next.vi 用于让迭代器向后移动一个节点:Enumerator Value.vi 返回迭代器指向的那个节点:使用这几个方法就可以搭建出一个简单的演示程序来看一下链表如何工作了。下面这个演示程序中,分两部分:第一部分是左面那个循环,每次循环迭代就会 创建出一个新的ListNode对象,它的数值是当前迭代的次数;右半部分使用链表的迭代器遍历链表中的节点。在这个演示程序中,迭代器移动次数比链表长 度多了两次,因为链表是环状的,转着圈访问,链表中的头两个元素会被读出两遍。程序运行后,data显示了迭代器每一步所指向的节点的值:回调VILabVIEW界面程序最常用的结构就是循环事件结构。用事件结构截获用户在界面上对控件的操作,然后做出相应处理。在文本语言中,常用的事件处理方法与LabVIEW是不同的。文本语言常常使用回调函数来处理界面事件。比如:某个按钮按下时,需要做一个fft运 算。那么就写一段函数来完成这个fft运算,再把这个函数与按钮按下事件关联起来。开发语言通常已经做好了对事件的监控,一旦发现按钮按下事件产生了,就 去调用与它关联的fft运算函数。这个有开发者编写,被系统调用的函数就叫做回调函数。LabVIEW也可以采用与文本语言相类似的方法来处理事件:不是在事件结构内处理,而是在程序开始时,就为某事件注册一个回调VI。在回调VI内编写相应代码,一旦事件发生,这段代码就会被执行。与事件结构相比,回调VI编写起来稍微麻烦一点;但它的好处是,它和主VI是平行运行的。如果事件处理过程比较耗时,把它放在事件结构中会阻塞整个程序,使得程序界面暂时失去响应;而把它放在回调VI中,则不会影响程序其它部分的运行。比如下面这个例子。程序界面上有两个仪表盘:左面那个始终在运转,每10秒钟旋转一圈;右边那个,由按钮控制,按下按钮才旋转一圈。若把旋转右表这个工作放到事件结构的按钮按下处理分支中去做,它势必会打断左表的旋转,因此,考虑把它放到回调VI中去做。这是主程序界面:两个表盘,和一个控制右表旋转的按钮。程序的代码也比较简单。先看代码的右半部份:这是一个典型的循环事件结构,用来控制左表的旋转。但是注意,右表的控制并不是在这个结构中实现的。再看程序左半部分:它为按钮“右表旋转一圈”的值改变事件注册了一个回调VI。注册回调VI用的是节点“Register Event Callback”,它在函数选板“Connectivity ActiveX”上。这个节点主要是为了给ActiveX、.NET控件的事件注册回调VI。事件结构无法截获ActiveX、.NET控件的事件,因此 只能通过回调VI的方式来处理这些控件的事件。但是这个节点也可以用于给LabVIEW自带的控件注册回调VI。注册回调VI节点,有三个输入参数从上至下分别是:事件的发出者、回调VI、用户自定义数据。在我们这个例子中,需要截获的是按钮“右表旋转一圈”的值改变事件,因此需要把“右表旋转一圈”控件的引用作为第一个参数传递给注册回调VI节点。 指定好事件的发出者,接下来需要选择事件的类型,鼠标点击注册回调VI节点的第一个参数的接线方块,发现“右表旋转一圈”按钮的所有事件都已经列在这里 了,选择“值改变”事件。第三个参数是用户自定义数据,可以是任意类型的数据,在回调VI中需要用到的数据都可以通过它来传递。因为我打算在回调VI中对控件“右表”做修改,因此,在这里把“右表”的引用作为数据传递给回调VI。第二个参数是回调VI的引用,如果已经写好了回调VI,把引用传进去就行了。我还没有编写回调VI,因此可以在参数的接线端上点击鼠标右键,选择“Create Callback VI”创建一个空白的回调VI。回调VI中写一小段代码,让右表旋转一圈,整个程序就完成了。这时,左右表可以各自运行,互不影响。LvClass 的一个效率问题前几天,听到了一个客户的抱怨:他编写了一个LabVIEW程序,每次打开主程序就要花费几分钟的时间,这有点令他忍无可忍。我没有见过他的源程序,不过据帮他检查过程序的同事讲,他的问题很可能是使用了大量的LvClass造成的。在他的项目中,包含有上百个类(LvClass)。我以前也听说过LvClass在效率上可能会有些问题,听到了这个消息后,我自己做了一个实验。LabVIEW Scripting中有一个属性节点可以用来查看内存中所有的VI,我就利用这个VI来查看一个程序到底在装入些什么,令它启动如此之慢。假设不存在子VI,如果打开某个不在LvClass中的VI(即便这个VI是属于某个lvlib的),只有这个VI会被装入内存。但是,打开某一个 LvClass中的VI,我发现不但这个VI会被装入内存,它所在的类中的所有其它的VI也都被调入内存。如果这个类还有父类和祖先类,那么所有父类、祖 先类中的VI统统都会被调入内存。总结一下就是这样:当一个VI被装入内存1. 它的所有子VI都会被装入内存;2. 它所在的类中的所有的VI都会被装入内存;3. 它所在的类的父类中的所有的VI都会被装入内存。以上3条可以是递归发生的,比如一个主VI A被装入内存,它的子VI B也会被装入内存,和B同属一个类的VI C也要被装入内存,C中有个子VI D,D属于类E,E有个父类F,F中有个方法VI G。尽管G的功能和程序A八杆子都打不着了,但也会被装进来。这大概就是那个用户遇到的问题,表面上他的程序不算太大,但是程序开始启动时,却需要把多于 程序本身数倍的不相关的VI都装入内存,这一过程会每次都浪费他几分钟的时间。鉴于LvClass的这一特性,设计使用它的时候一定要格外小心,否则很可能会造成程序效率的低下。我想到了几点需要注意的地方:1. 如果仅需要对一些VI进行封装,那么应当使用lvlib,而不是lvclass。两者封装的主要区别是,lvclass可以封装对象的属性(也就是模块用到的数据)。2. 类中的VI必须是高内聚的,类中的方法共同完成某一基本功能,不可再分割。应用程序一旦用到这个类中的某个VI,就意味着程序将会使用到类中几乎全部的VI;而不是一个应用程序可能只使用这个类中的某几个VI。3. 继承关系应当尽量简单。没有必要的时候尽量不使用继承。LabVIEW不支持接口,不应创建一个纯虚类,然后当作接口来用。4. 尽量不要嵌套调用。比如在一个类的VI中又去调用另一个类中的VI。5. 打算使用多态这个特性时要注意,多态使得应用程序在运行时,根据对象的类型选择对应的处理方法。但有些选择应当是程序编译时就做出的,它们不适合套用在多态特性上。举一些例子: INI文件读写这个模块比较适合做成类,每个INI文件对应一个类的实例。它有丰富的数据(文件的内容);它的方法有限,基本上只需要打开、读条目、写条目、保存关闭,这四个方法,并且一般的应用程序都会同时使用到这四个方法。 复杂仪器的驱动程序不适合做成类。因为驱动程序会提供非常多的功能,示波器有各种触发模式。而一个应用程序通常只用到多种模式中的某一种就够用了。 某测试程序可以生成测试报告给用户。用户可以选择几种不同的报告类型。生成报告的模块可以用lvclass来设计。因为生成不同类型的报告的方法间,可重用代码很多,可以为它们设计一个基类。并且,是程序运行时,才选择生成报告类型的。 某一测试程序,可以支持多种型号的仪器。因为不同用户使用不同的硬件。对不同型号仪器的支持不适合使用lvclass来设计,因为测试程序发布给用户时,用户的硬件设备是固定的。对仪器的选择应当是程序发布时就决定好的,而不应等到程序每次运行起来后判断。如何在程序中同时弹出多个子VI的界面,各自运行互不影响回答网友一个问题:“我设计了一个labview界面子VI,我想在主VI中多次调用该界面VI(同时执行,单独分配内存),并显示出多窗口,该如何设置?我尝试将子VI属性设置成可重入,仍无法解决。”这个问题其实挺常见的。若需要子VI打开多份实例,子VI必须是可重入的。所以第一步要把子VI设置为可重入。但仅仅这样还不够,主程序运行到子 VI处,把子VI打开后,会一直等在这里,知道子VI运行结束,才继续执行主VI后续的代码。主VI既然已经停在这里了,自然不会再继续去打开其它子 VI。解决的办法是在调用子VI的地方,改为动态调用,并且不等待子VI运行结束。这样一来,主程序运行到这里,将子VI调起后,立即执行后续代码,又可 以去调用其它的子VI了。需要注意的是,用于子VI是可重入的,需要给“Open VI Reference”函数设置一个值为“8”的Options参数。主VI程序代码如下:LabVIEW 2009 新功能 传引用以前版本的LabVIEW虽然也有多种方法可以让数据以引用的方式在程序间传递,但是用起来都有些麻烦。LabVIEW有了构建数据传引用的节点,大大简化了传引用的程序代码。新添的有关传引用的两个节点在函数选板“Programing-Application Control-Memory Control”中,分别是“New Data Value Reference”和“Delete Data Value Reference”。“New Data Value Reference”用于创建一个数据的引用,“Delete Data Value Reference”可以从引用中取回原来的数据。引用最主要应用于多线程程序中。如果两个线程同时对同一份数据进行修改,则必须使用传引用的机制。否则,使用值传递的方式,数据在数据线分叉的地方,就会编程独立的两份,之后在两个线程内分别修改的是两份完全独立的数据,没办法对同一份数据进行修改的。比如下图这个程序,程序输入了一个数组,然后需要在两个并行的子VI中同时对这个数组中的数据进行修改。每个子VI可能修改了数组不同的元素,程序运行结 束产生的数组应当把两个子VI中的修改都包含进来。因此,程序一开始需要数组数据生成一个引用,然后把引用分别传递到两个子VI中去。两个子VI都运行结 束后,在从引用中取回数据。LabVIEW中已有的函数还都是为值传递设计的,所以使用值传递少不了把数据取出、放回的过程。这以过程中,可能又会产生数据拷贝,效率会比较差。好在 我们可以使用“In Place Element Structure”结构来处理从引用中取出、放回数据的过程。配合了“In Place Element Structure”结构使用后,LabVIEW会尽量使用数组原地址,而不是把从引用中取出的数据复制一份,这样就做到了传引用与效率的兼顾。比如下面 两图中的程序,功能是完全相同的,但LabVIEW会对下面一幅图中的程序进行优化,提高效率。美化程序 隐藏程序框图上的大个 Cluster在编写某些程序的时候可能会遇到如图1 所示的情形:即用到了一个极为复杂的数据类型常量。 这个常量由于体积巨大,使得在程序框图无论怎么摆放都让人看起来不太舒服。如何才能把这个程序改造得美观一些呢?图1:体积巨大的常量会有碍观瞻要解决这个问题,只有设法把这个常量在主程序框图上隐藏起来。通常可以用以下两种方法。第一种方法:把这个常数变换成控件,再把控件隐藏起来。这种方法比较简单,但是也有弊病。容易引起误解:控件一般表示有值传入,其他人读程序读到这里就 可能搞不清楚这个值是从哪里传来的了;如果要修改常量 Cluster 中某一个元素的值,操作起来比较麻烦。第二种方法,也就是我向大家推荐的:把它隐藏到更深层的子 VI 中去。具体操作方法如下:如图2 先给这个复杂数据类型建立一个 Strict Type Def。我的建议是为所有程序中用到的 Cluster 都建立一个 Strict Type Def。这样可以为以后的程序维护省去很多麻烦。图2:Strict Type Def.然后然后再建立一个新的 VI,把我们要隐藏的这个个头巨大的常量摆放在这个 VI 中,并且连接一个 Indicator ,以把它的值传出来。VI 的接线板采用 4-2-2-4 格式的,最下层第 3 个接线端用于传出 VI 中唯一的数据,如图3 所示。图3:用于隐藏个头巨大常量的 VI这个 VI 的图标要做得小巧漂亮,如图4,图标不一定非要做成正方形。只要 B&W 和 256 Colors 中的图标形状一样,我们就可以画出不规则图标了。详细方法可以参考制作不规则图形的子VI图标。图4:常量数据 VI 的图标把这个新造出来的常量数据 VI 拖到程序框图上,把它的输出链接到刚才链接常量的地方,再把位置摆放好。现在我们的程序是不是漂亮多了图5:改造后的程序框图Caption 和 Label 的书写规范LabVIEW控件的 Caption 和 Label 的特性和用途很相似,都是给了控件一个有意义的名字。因此,在很多场合没有必要刻意区分他们。Caption 和 Label 的最主要区别在于,Caption 可以在程序运行的时候改变;而 Label 则不可以,一旦程序运行,就固定不变了。鉴于这一点,Caption 和 Label 的用途也略有区别。Label 应该是给程序自己用的,比如在程序中需要根据控件的名字找到它,那就得跟据 Label 来找,而不能用Caption来找;Caption 是为了给用户看的,有时控件的名字在运行到不同状态下需要发生改变,此时显示在界面上的就应该是 Caption。推荐大家按照下面的规范使用 Caption 和 Label。先给 VI 分一下类: 1.底层 VI:用户不会直接使用到的 VI,作为 subVI 随程序一起发布。 2. 用户界面 VI:VI 前面板是给用户看的程序界面的一部分。 3. 程序接口 VI:VI 是提供给用户,在他们编程时,当作API 被调用。对于 Caption 和 Label 一个共同的书写规范是:使用有意义的文字,在使用英语短语命名时,单词之间用空格分隔,不应该有重名。不同点列于下表:LabelCaption底层 VI显示出来使用 LabVIEW 的默认状态,即 Caption 为空。用户界面 VI隐藏多语言版本中,只使用英语显示多语言版本中,使用本地化语言程序接口 VI隐藏多语言版本中,只使用英语不用标注控件的默认值显示多语言版本中,使用本地化语言在后面加一括号,括号内标注控件的默认值和数据单位在 LabVIEW 中实现 VI 的递归调用LabVIEW 中使用递归调用不是很方便。不过递归并不是编程必须程序结构,任何需要使用递归调用的地方,都可以用循环结构来代替。但是在某些情况下,使用递归调用的确 可以大大简化程序代码,对缩短编程时间、提高程序可读性都非常有帮助,所以学习一下递归的实现方法还是有好处的。一、为什么 VI 不能够被静态的递归调用LabVIEW 不能通过静态调用的方法(把子 VI 直接放到另一 VI 的程序框图上)来实现递归。对于一个非可重入的 subVI,在每一个时间,这个 subVI 这能被运行一次。LabVIEW 需要借此来保证多线程时的数据安全。对于被递归调用的代码,是需要在它执行到中间的时候,就再次被调用的。所以默认设置下的 VI 不能被静态递归调用。对于被设置为可重入的 VI,是可以被同时调用多次的,但也不能被静态的递归调用。除非是通过 VIServer 动态的调用 VI,否则,LabVIEW 是在一个程序被调入内存,开始运行之前就为它的所有 VI 分配好内存空间的,包括数据区。如果一个 VI 不是可重入的,LabVIEW 会在这个 VI 运行时局部变量所在的数据区开辟在这个 VI 所在的空间内;对于可重入的 VI,LabVIEW 把它的数据区开辟在调用者 VI 上,这样就可以保证这个可重入 VI 在不同的地方被同时调用时使用不同的数据区,以防止多线程运行时数据混乱。因此,可重入 VI 虽然可以被同时多次调用,但是被调用的次数是运行前就确定的。而递归运算时的调用次数是运行时决定的。这样,如果是静态调用, LabVIEW 根本没有办法为提前为参与递归的 VI 开辟好数据区。二、用动态调用方法实现递归图1 是一个采用递归算法计算阶乘的例子,可以点击后面的连接直接下载示例 VI:3535080153251.rar。图1:利用递归结构计算阶乘正如前文说过的,所有的递归都可以使用循环来代替,计算阶乘也可以使用循环结构,但是这里介绍的是使用递归结构的方法。因为 n!=n*(n-1)!,所以我们只要编写一个 VI 实现功能 F(n)=n*F(n-1) 就可以了。程序中,递归调用 VI 自身的结构由三个 VI 动态调用节点实现:Open VI Reference, Call By Reference Node, Close Reference。这三个节点分别负责动态打开一个 VI(本例中就是这个 VI 自身),运行这个VI,再关闭它。使用 Call By Reference Node 需要在打开 VI 句柄的时候就要知道 VI 连线板(Connector Pane)的布局,因此,我们在用 Open VI Reference 打开 VI 的时候要提供 VI 连线板的布局信息,在例子中就是 Open VI Reference 节点上方的那个常量。三、使用递归时的几点注意事项递归调用的退出或结束条件,本例中当输入数据小于1时,就需要结束递归调用返回最底层的值了。如果递归调用的退出条件设置不当,可能会引起程序死循环甚至崩溃。LabVIEW 中也可以实现 A 调用 B,B 又调用 A 这种用多个 VI 相互调用的递归结构。参与递归调用的 VI 必须被设置为可重入。动态调用的需要把 VI 在运行时调入内存,这个过程是比较耗时的。因此递归结构的运行效率远不如可实现相同功能的循环结构,内存占用也会更大一些。决定使用递归结构之前要考虑到这些因素。在 LabVIEW 中实现 VI 的递归调用LabVIEW 中使用递归调用不是很方便。不过递归并不是编程必须程序结构,任何需要使用递归调用的地方,都可以用循环结构来代替。但是在某些情况下,使用递归调用的确 可以大大简化程序代码,对缩短编程时间、提高程序可读性都非常有帮助,所以学习一下递归的实现方法还是有好处的。一、为什么 VI 不能够被静态的递归调用LabVIEW 不能通过静态调用的方法(把子 VI 直接放到另一 VI 的程序框图上)来实现递归。对于一个非可重入的 subVI,在每一个时间,这个 subVI 这能被运行一次。LabVIEW 需要借此来保证多线程时的数据安全。对于被递归调用的代码,是需要在它执行到中间的时候,就再次被调用的。所以默认设置下的 VI 不能被静态递归调用。对于被设置为可重入的 VI,是可以被同时调用多次的,但也不能被静态的递归调用。除非是通过 VIServer 动态的调用 VI,否则,LabVIEW 是在一个程序被调入内存,开始运行之前就为它的所有 VI 分配好内存空间的,包括数据区。如果一个 VI 不是可重入的,LabVIEW 会在这个 VI 运行时局部变量所在的数据区开辟在这个 VI 所在的空间内;对于可重入的 VI,LabVIEW 把它的数据区开辟在调用者 VI 上,这样就可以保证这个可重入 VI 在不同的地方被同时调用时使用不同的数据区,以防止多线程运行时数据混乱。因此,可重入 VI 虽然可以被同时多次调用,但是被调用的次数是运行前就确定的。而递归运算时的调用次数是运行时决定的。这样,如果是静态调用, LabVIEW 根本没有办法为提前为参与递归的 VI 开辟好数据区。二、用动态调用方法实现递归图1 是一个采用递归算法计算阶乘的例子,可以点击后面的连接直接下载示例 VI:3535080153251.rar。图1:利用递归结构计算阶乘正如前文说过的,所有的递归都可以使用循环来代替,计算阶乘也可以使用循环结构,但是这里介绍的是使用递归结构的方法。因为 n!=n*(n-1)!,所以我们只要编写一个 VI 实现功能 F(n)=n*F(n-1) 就可以了。程序中,递归调用 VI 自身的结构由三个 VI 动态调用节点实现:Open VI Reference, Call By Reference Node, Close Reference。这三个节点分别负责动态打开一个 VI(本例中就是这个 VI 自身),运行这个VI,再关闭它。使用 Call By Reference Node 需要在打开 VI 句柄的时候就要知道 VI 连线板(Connector Pane)的布局,因此,我们在用 Open VI Reference 打开 VI 的时候要提供 VI 连线板的布局信息,在例子中就是 Open VI Reference 节点上方的那个常量。三、使用递归时的几点注意事项递归调用的退出或结束条件,本例中当输入数据小于1时,就需要结束递归调用返回最底层的值了。如果递归调用的退出条件设置不当,可能会引起程序死循环甚至崩溃。LabVIEW 中也可以实现 A 调用 B,B 又调用 A 这种用多个 VI 相互调用的递归结构。参与递归调用的 VI 必须被设置为可重入。动态调用的需要把 VI 在运行时调入内存,这个过程是比较耗时的。因此递归结构的运行效率远不如可实现相同功能的循环结构,内存占用也会更大一些。决定使用递归结构之前要考虑到这些因素。一个 XControl 的实例XControl与 .ctl 用户定义控件相比,其最大的提高就在于它不但可以定义控件的外观,还可以定义控件的行为。在 XControl 出现之前,同样可以在程序中编写代码,控制程序的行为。在用 XControl 实现面向组件的编程一文中提到了,这种方法在程序模块划分上有缺陷。如果用户想发布一个带有特定行为的控件也是不可能的,因为控制控件行为的代码,是同其它代码混杂在一起的。利用 XControl 可以解决上面提到的问题,这里以一个例子说明一下如何利用 XControl 实现一个有特定行为的控件。Windows 风格的工具条上的按钮有一个特点,就是当鼠标移动到按钮上方,按钮就会变亮或浮起。LabVIEW 中默认的按钮没有这样的特性,但是实现这一点是很容易的。以鼠标移上,按钮变亮为例:在程序中,当按钮的 Mouse Enter 事件发生时,把按钮的颜色设置为浅颜色;当按钮的 Mouse Leave 事件发生时,把按钮的颜色设置为深色即可。现在把界面上的按钮和控制颜色的代码都封装在一个 XControl 中。这样,其他人在使用这个 XControl时,就无需修改他的代码,而直接获得这种颜色变化的特性了。一、简单行为的 XControl首先创建一个空的 XControl。 图1、2:创建一个新的 XControl新的 XControl 中有四个 VI。Data.ctl 定义 XControl 的数据类型。比如我们要做一个按钮,数据类型应该是布尔型。如果要作一个工具条,数据类型就应该是布尔型数组了。State.ctl 定义 XControl 内部要用到的一些数据,类似于类的私有变量。我们这个简单的例子用不到任何变量,所以可以不去动它。Init.vi 类似于类的构造函数。在我们这个简单的例子中也不需要去改变它。Facade.vi 是最主要的 VI,XControl 的外观和行为都是在这个 VI 中定义的。Facade.vi 的界面就是 XControl 控件的外观。控制控件行为的代码也是放在这个 VI 的程序框图上。我们要做的是个按钮,所以就在 Facade.vi 的前面板上放一个按钮。如果希望用户在使用这个 XControl 时可以调整它的大小,在我们这个简单例子中,只要设置 Facade.vi 窗口尺寸属性中的“在窗口尺寸变化时,按比例调整控件大小”这个选项就可以了。对于复杂的 XControl 控件,要另写代码,在窗口尺寸变化后重新计算每个控件的大小和位置。图3:窗口尺寸属性设置控制按钮颜色的代码也需要放在 Facade.vi 中:把前文提到的按钮的 Mouse Enter 和 Mouse Leave 放在这里即可。具体实现方法,可以参考文章结尾给出的范例程序:5280261469140.zip。二、有持续运动的 XControlFacade.vi 不能够持续运行,只有在有事件发生时,LabVIEW 才会调用这个 VI。处理完这个事件,Facade.vi 就会停止运行。不要试图让 Facade.vi 持续运行,否则会导致整个 LabVIEW 被挂起。有时候,需要控件能够循环地或者持续一段时间地作一个动作。比如说,需要做一个不停闪烁的小灯。控制灯光闪烁的代码就不能够放在 Facade.vi 中。实现这种功能的一个方法是:把定时控制小灯颜色的代码放在一个可重入 VI 中,通过小灯控件的引用参考来定时更改它的颜色属性。在 XControl 的 Init.vi 中把这个定时 VI 动态加载并以异步方式运行;在 XControl 的 Uninit.vi 中再把这个定时 VI 卸载即可。Uninit.vi 不是一个必须的 XControl 功能定义 VI(Ability VI),新建的 XControl 没有这个 VI。可以在工程浏览窗口,鼠标右击这个 XControl 来为它添加新的功能定义 VI。5279316579099.zip,它只能在 LabVIEW 8.5 下打开。XControl 是可以在 VI 的面板上放多个实例的,每个实例小灯的闪烁频率可能不同。我在这个例子里,每个 XControl 实例都有自己的一个专用定时 VI,因为这些 VI 是可重入的。定时的方法我采用的是加延时。我做了一下测试,发现现在的 XControl 有个问题,就是在程序面板上放多个 XControl 实例之后,定时就变得非常不准确了,小灯闪烁速度明显减慢。这也许是 XControl 的 bug,也许是 LabVIEW 延时函数的问题。解决这个问题的方法就是使用一个定时 VI 控制所有的实例,当然这样的实现方法会比较麻烦一些。下载文章中的示例程序: /examples/a-button-highligh-when-mouse-moves-in/ /examples/4e0076f495ea70e-a-twinkled-buttonLabVIEW 中的泛型容器Google 网站里有个 Google 实验室,有不少 Google 的产品最初就是放在这个实验室里的。现在 NI 也有 NI 实验室了。 NI 实验室公布出来的项目一般是 NI 工程师利用额外时间做的一些调查研究。这些项目不是公司的正式产品,但是它们的设计很有创新或者是比较有应用潜力。与其让这些项目被埋没了,不如先看看用 户对这些项目的反应,如果相当一部分用户觉得某个项目非常有帮助,或许它就值得我们为其增加投资,把它作成正式产品了。我这里给大家介绍其中的一个项目:“LabVIEW Generic Container Map”。因为这个项目是我设计的,所以对它了解比较多一些。当时,我们打算提出这个项目的时候,主要有两个目的:第一是帮助用户编写有复杂数据结构的应用程序;第二是推进 LabVIEW 向通用编程语言方向做改进。C+ 的程序员基本都很喜欢 STL 这个模板库。程序中常会使用数组、队列、字符串等等数据类型和结构,如果自己设计实现这些数据结构和相关的操作,是相当耗费精力的。好在 STL 实现了这些数据结构,和它们常用的操作方法。借用 STL 提供的功能,编程时很多细节方面不需要再去考虑了,这就让工作简化了许多。尝到 STL 甜头的程序员,在编写程序的时候,已经很难离开 STL 了。STL 中非常重要的一个部分就是容器。容器用于存放数据,程序通过调用容器的结构函数保存数据到容器或者访问容器中的数据。容器也分为不同的类型,如链表、队列 等。它们在数据的组织方式上,或存取方式上有所分别,以适用不同的需求。STL 中的容器和方法都是泛型的或者说是数据类型无关的,就是说这些容器可以保存和操作任何类型的数据。其它一些常用的编程语言,如 Java、C# 也都有类似的泛型容器以方便程序员使用。LabVIEW 的主要方针是简化工程师们编写程序的难度,以前用 LabVIEW 编写的程序大多是工业领域流程控制类型的。这种类型的程序用不到太复杂的数据结构和算法,因此,LabVIEW 中对我们在计算机课程中学到的那些经典数据结构以及算法的支持并不多。但是在我自己用 LabVIEW 多了之后,用它比用 C+ 要顺手,任何类型的程序都喜欢使用 LabVIEW 来编写,包括一些通常用途的程序。这时候,LabVIEW 缺乏对基本数据类型支持的缺点就格外突出了。于是我和周围几个同事就想到应该在这些方面对 LabVIEW 做一些补充,做一些比较规范的泛型容器和算法,一方面方便自己,也许还可以提供给别的用户。由于这不是正式项目,我们能投入的资源很有限,不可能一开始就做得很全面。作为开始,我们选择了 Map 容器和它最常用的几个方法。首先选择 Map 一是因为它比较常用,二是其它容器中,有些在概念上和 LabVIEW 中已有的一些函数比较接近,如果选则他们,可能会引起用户的误解。LabVIEW 中的 Array 操作与 STL 中的 vector 是非常相似的,功能齐全,不需要考虑底层操作如内存管理等。STL 中的 deque, queue, stack 等,与 LabVIEW 中的“队列”(Queue)操作比较类似。但是 LabVIEW 中的队列存在的目的不是为了作容器,而是用于在多线程程序中通讯。在“生产者/消费者”程序模式中,经常使用队列在不同的线程中传递数据或消息。因为 LabVIEW 中队列操作主要用于不同线程间的通讯,因此它的函数并没有采用 LabVIEW 的主要传参方式-传数据,而是采用了传引用的方式。我们实现的这个 Map(这个按字面翻译比较别扭,中文可能翻译成“字典”还比较合理)泛型容器功能与 C+ STL 中的 Map 是类似的,它主要用于程序经常需要按某一关键字查询数据的情况。Map 已经包括了编写查询程序时常用的操作,比如把数据放到容器中、查找一个
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 仓储物流信息化管理与运输服务合同
- 跨国公司境内股权转让及税务筹划协议
- 生态柴油购销合同范本与规范
- 成都租赁合同(含租客租后押金退还)
- 民宿民宿风格改造装修合同
- 互联网保险保本投资协议
- 北京二手房交易税费减免咨询与代理合同
- 餐饮店拆伙协议及员工安置合同
- 时尚购物广场门面房租赁与品牌合作合同
- 肿瘤的影像学诊断
- 2025年中国邮政集团有限公司辽宁省分公司人员招聘笔试备考试题及答案详解1套
- 充电站建设管理制度
- 美好生活大调查:中国居民消费特点及趋势报告(2025年度)
- 失业保障国际比较-洞察及研究
- 党群工作笔试题目及答案
- 黑龙江省2024年普通高校招生体育类本科批院校专业组投档分数线(物理类)
- 2025年陕西延长石油矿业有限责任公司招聘笔试参考题库含答案解析
- 天津市部分区2023-2024学年八年级下学期期末练习道德与法治试卷
- 国开电大《Java语言程序设计》形考任务三答案
- 初一几何综合练习题
- 综合实践活动评价表完整
评论
0/150
提交评论