近日在LABVIEW的群中,看到几次讨论利用队列把采集的数据写入到数据库的问题,讨论非常热烈,但是他们忽略了一个重要的问题,就是数据库的最大写入速度的问题。

一旦你每秒采样数超过一定限度时,无论你采用何种编程方式,如何优化你的程序,在超过一定限度后,都是不可能实现的,在这种情况下,讨论如何解决就毫无意义了。

队列主要有三方面的用途,并行、解除耦合和解决忙先不均。在高速采集时,队列只是起到了并行的作用。由于采集的速度远大于数据库的写入速度,这种情况下,使用数据库本身就是一个错误的选择,是不可能实现的。

下面我利用LABVIEW本身的数据库例程,稍微改动一下,测试数据库的写入速度。LABVIEW数据工具包提供三种不同的写入数据方式。

1、自动方式。利用“插入数据”VI。

2、利用SQL查询方式。

3、利用SQL参数化查询。

这三种方式中,SQL查询方式速度明显是最快的。SQL参数化查询和自动方式非常接近,SQL参数化查询比自动化方式略快一些。

在我的计算机中,写入10000条记录,SQL查询方式约为20秒,其它方式约为30秒。也就是说使用MDB数据库,每秒最快写入约500条记录。

我写入的记录数据量是很小的,如果记录中存储的数据量很大,速度还要慢。所以,使用数据库仅仅适合与低速采集,对中高速采集是不可能实现的。

LABVIEW的DSC工具包采用了通用数据库,因为常用的扫描周期为100MS,这是没有问题的。对于中高速采集,提供了S存储方式。即使采用TDMS方式,也必须在一定限度之内,毕竟最终的速度还是取决于硬件设备。

 
几个月前,LABVIEW7I老师(高巍)向我推荐了一份PPT,其中主要讨论了性能与内存管理的问题,这是NI公司内部团队写的,因此非常珍贵。 LABVIEW对用户屏蔽了内存使用的细节,作为用户很难了解内存是如何使用的以及如何提高程序的运行性能。这份PPT是难得一见的性能与内存管理方面的 资料,所以高老师希望我能翻译其中重要的部分,介绍给大家。

对于英文资料,在翻译的同时,不可避免地会参杂一些个人的理解,如果对其中的内容有歧义,欢迎大家留言讨论。

性能与内存管理

目标:

  • 理解LABVIEW执行系统
  • 学习如果通过减少数据拷贝以及减少总的内存使用来改进性能
  • 了解VI的执行特性

LABVIEW执行系统

  • 执行系统是LABVIEW重要的组成部分,负责实际运行我们的代码。
  • 启动自动并行机制
  • 对于LABVIEW来说非常特殊---其它编程语言需要人工进行线程管理。
LABVIEW 执行系统可以使我们的代码自动并行运行。在其它编程语言中,必须通过人工方式进行多线程管理,但是LABVIEW在可能的情况下,编译器和执行系统共同写 作,自动并行运行我们的代码。在大多数情况下,执行系统对我们来说并不是很重要,因为执行系统不需要我们参与就会做的很好。

但是在某些情况下,我们有必要了解执行系统是如何工作的,因为这有助于我们改进程序的运行性能。

  • 执行系统的工作类似一个线程池。一个任务队列和一个线程集合提取任务出队列。
  • 任务(“队列元素”)就是VI代码需要执行的片段。
  • 每个执行系统对应一个队列,包括UI执行系统、标准执行系统、仪器执行系统、数据采集执行系统、其它1执行系统、其它2执行系统和定时循环执行系统
LABVIEW执行系统的工作类似一个线程池。所谓线程池是多个线程的集合,这些线程协同工作,通过从一个共享的队列,共同完成从队列中提取的任务。在LABVIEW中,这些任务称作“队列元素”,每个队列元素代表一小段需要执行的代码。

LABVIEW实际拥有6个通用的执行系统,每个执行系统拥有一个独立的队列。另外,每个定时循环拥有自己独特的执行系统。

  • 每个执行系统具有多个线程
  • UI执行系统是个例外,它只有一个线程。
Picture
上图是LABVIEW执行系统如何工作的模型图。对于每个执行系统(上图中示意了三个执行系统),我们各自创建了一个队列。入队的数据代表了VI代码中需要执行的片段。当这个需要执行的片段准备好要运行时,LABVIEW把它放入到某个执行系统的队列中。

每个执行系统包括一个或者几个线程,这些线程中一个包括一个循环,负责把代码片段出队列并执行之。UI执行系统只拥有一个UI线程,其它执行系统可以拥有多个线程,但是共享 一个队列。当VI代码并行运行时,由执行系统的不同线程进行管理。


Picture
除了操作系统的抢先式多任务系统外,LABVIEW引入了协作式多任务系统。在编译过程中,LABVIEW分析VI,定位那些可以共同执行的节点 组,称作CLUMPS,每个由优先级和执行系统组成的运行数据队列结构把这些可以共同运行的CLUMPS保存在一起。当执行系统激活一个线程时,执行系统 解析这些CLUMPS并执行之。当执行系统执行完后,运行队列存入其它的符合输入条件的CLUMP进队列,这使得程序框图可以在任意的有效线程中运行。如 果程序框图包含了足够的并行机制,就可以在所有的线程中同时运行。

LABVIEW并不是对某个CLUMP分配特定的线程,在VI再次运行 时,同样的CLUMP可能被分配到不同的线程中。每个CLUMP都会产生一小段代码由LABVIEW调度。在CLUMP内部,LABVIEW不会使用并行 机制,在CLUMP之间,LABVIEW利用执行系统执行多任务。



Picture
上图展示的CLUMP代表了程序框图开始到结束的过程,当执行中间两个FOR循环时,CLUMP处于“睡眠”状态。然后被“唤醒”完成VI(执行两个除法运算)。

 当一个节点进入“睡眠”状态时,它把自己放入一个等待队列中,然会再返回到执行系统中。当等待结束后,它脱离等待队列,再次放入到执行队列中。



首选执行系统

  • 某些节点必须运行在U线程中
  • 每个VI都可以指定首选的执行系统,默认情况下选择“与调用者相同”
某 些节点具有首选的执行系统。最为常见的首选执行系统是UI执行系统。有些节点必须运行在UI线程中,比如VI服务器属性节点、VI服务器调用节点和打开 VI引用节点必须工作在UI线程中。DAQ中的属性节点和调用节点不在UI线程中,LABVIEW中的类的属性节点和调用节点也同样不在UI线程中。

另外,每个VI都可以选择首选的执行系统,默认情况下为“与调用者相同”,这意味着VI可以运行在任何执行系统中。



切换执行系统

  • 当代码需要工作在不同于调用VI的执行系统,或者此前运行的执行系统时,就发生了执行系统的切换。
  • 通常发生在与UI操作有关的代码中。
  • 切换执行系统会导致运行性能的问题。需要进入睡眠状态,然后在其它执行系统   中被唤醒。切换会产生耗时。
  • 避免不必要的VI代码
当一个节点需要运行,但是节点需要运行在另外的执行系统(非当前运行)中时,这会导致队列元素处于“睡眠”状态,并在其它的执行系统中被唤醒。这意 味着这段代码将停止运行,把自己放入到希望运行的那个执行系统队列中。当那个执行系统有时间时,执行系统从队列中取出元素并执行之。这种执行系统的切换需 要耗费时间,可能会引起性能的下降。

为了避免这种执行系统的切换,我们应该避免不必要的UI代码(比如服务器属性节点)。对子VI使用“与调用者相同”的执行系统可以有效地避免不必要的执行系统切换。

在下列两种情况下,我们可能会对VI使用特定的首选执行系统.

  1. 如 果VI内部含有必须在UI线程执行的代码,当我们使用“与调用者相同”选项时,VI执行时必须切换到UI线程,然后再返回到调用方的执行系统,以便结束子 VI时,能够继续调用方的执行系统继续运行。假如子VI运行的一个循环时,这会导致子VI内部不断反复切换执行系统,极大影响程序的性能。这种情况下,如 果对子VI选择工作在UI线程,则进入子VI之前切换到UI执行系统,在循环中一直在UI执行系统下运行,在退出之前切换到调用者的执行系统。这样执行系 统的切换只会在进入和退出前发生两次,可以极大提高程序的运行性能。
  2. 如果一个VI内部存在一个长时间的循环运行的任务,如控制数据采 集,我们可以设定VI工作在其它执行系统(仪器执行系统、数据采集执行系统、其它1执行系统或者其它2执行系统)。这样这些任务就不会和其它VI竞争获取 执行系统操作时间。我们需要注意,如果我们不是工作在实时操作系统中,我们还要考虑操作提供本身的调度问题。通过控制定时循环的优先级可以有效地控制我们 代码执行的优先执行次序,在实时操作体统中,定时循环的优先级更为可靠,也更加明显。

优先级

  • 子VI的优先级影响执行系统中队列元素的优先级。
  • 高优先级的队列元素优先出队列。
  • 子VI的优先级高低设置并不影响执行系统线程自身的优先级。

子程序优先级

  • 子程序优先级并非真正的优先级。
  • 对于经常被调用的代码可以减少执行系统的开销。
  • 强制把整个VI放在一个单独的CLUMP中。
  • 阻止VI进入睡眠状态。
  • 没有并行机制
  • 可以被设置为“如果忙,去除子程序设置”。
  • 通常不推荐使用。
子程序优先级并非真正的VI优先级。VI的优先级是通过改变队列中元素的优先级实现的,而子程序优先级是把整个VI放入在一个CLUMP中,这就保证了 VI一旦执行就不会进入睡眠状态。这意味着我们不能在子程序内部调用有可能导致进入睡眠状态的其它子VI,在VI内部也不能引发执行系统的切换。设定为 “子程序优先级”的VI在内部只能调用同样设置为“子程序优先级”的其它子Vi。“子程序优先级”的VI的代码在内部执行时都是按照次序执行,而不是并行 运行。

内嵌(内联)VI

  • 首选用于替代子程序优先级
  • 当主调VI编译完成后,整个设置为内嵌的子VI的全部代码插入到主调VI中。这样就不存在调用的开销了。
  • 依然支持并行运行机制。
  • 允许更多的编译优选项。
  • 存在下列限制:不允许访问前面板,仅支持部分节点,当子VI反生变化时,强迫主调VI重新编译。

封锁线程

  • 在执行调用期间,外部代码(如调用库函数、CIN节点、NET、ACTIVEX等等)会锁定线程。
  • 由于C代码不使用LABVIEW队列元素,所以运行时会锁定线程。
  • 等待期间,其它VI无法运行。
  • 一般情况下,无法中止外部代码的操作。
LABVIEW的执行系统是基于多线程相互协作的基础上,但是其它语言编写的代码并非如此。这意味着当我们从LABVIEW中调用外部代码 时,LABVIEW只有在外部代码操作完成后才能继续线程操作。当外部代码需要等待运行权限或者其它原因进入等待状态时,当前的执行系统必须等待外部操作 代码运行结束,因此等待期间,执行系统内不会有其它VI运行。如果外部代码无法完成,经常会导致出现“重置VI”对话框。如果使用调用库函数节点调用外部 代码,则需要在配置对话框中的“回调”也选择合适的选项,避免这个问题的发生。



连线的含义

  • 每一条连线都代表一个缓冲区。
  • 连线分支会导致数据拷贝。
Picture
LABVIEW的数据流编程模式意味着每一条数据连线都操作着它自身的数据拷贝,在子VI内部同样也需要创建数据的拷贝。由于两个分支创建了数据拷贝,因此两个分支可以同时独立工作,不 会出现数据锁定的情况。

Picture
在上图的示例中,按图索骥需要5个数组的拷贝。但是实际上LABVIEW仔细分析这段代码,发现可以分为上下两个分支,分别执行加法操作和乘法操作。上面 分支中,表明的部位(+)产生一个拷贝,因为加法操作将改变输入部分的数组,对输入的数组具有破坏性。下面的分支LABVIEW进行了优化,乘法输出和加 法输出采用同地址操作,使用相同的数组。可以看出,经过LABVIEW的优化,需要5个拷贝变成了只有一个拷贝。这个优化方案是非常理想的,因为上下两个 分支都需要把数据写回数组,所以不管使用那种编程语言,都需要至少一个拷贝的过程。

同地址算法

未完待续

 
最近GSD论坛上的一个帖子谈到了全局变量的问题:

  • sandan0615:
在保证数据不冲突的情况下可以对全局变量写操作吗?
RTRT,各位高手解释下,我在陈树学老师的宝典里看到说在程序里要避免对全局变量进行写操作。
  • czhen:
当然可以
不行的话,要它干啥

  • wyb4993:
我有一个自动化测试程序,里面有很多LabVIEW全局变量,可以读和写。是前一任离职的兄弟留下的。目前运行很正常。


关于慎用全局变量的问题,很多编程语言方面的书籍都会提及,NI论坛上有一个长达十几页的帖子专门讨论的这个问题,非常详细。
其中不仅仅涉及全局变量,还提及了许多解决问题的技巧。
下面我大概翻译一下其中重要的部分,希望有助于理解如何正确使用全局变量。帖子很长,我需要用几天的时间陆续给大家介绍。
------------------------------------------------------------------------------------------------------------------
TBOB:
不止一次地看到人们在抱怨,全局变量是罪恶之源,根本就不应该使用它们。但是我不认为这个结论是显而易见正确的。我希望能听到一次有关全局变量的严肃认真的讨论。论坛中的朋友们很多都提到了他们都正在编程中使用全局变量。
先从全局变量的有点谈起。一般来说,全局变量是公认的在各个VI之间传递数据的有效方法,比起其它方式的全局变量(个人意见)更容易管理,因为假如我们使用了一个簇作为全局数据,我们没有办法确定在何处使用了它们,可能需要自己创建一个文件记录它们使用的位置。但是全局变量则不然,通过全局变量的右键快捷菜单,我们可以很容找到引用全局变量的位置。

使用全局变量有两个不利之处,其一,引用全局变量需要创建数据的拷贝,这可能会导致潜在的竞争条件或者导致数据的丢失。其二,使用全局变量会中断数据流程。
所以,我对那些反对使用全局变量的人士提出一个问题---你们在应用全局变量时考虑了全局变量是否有效的问题了吗?
如果对全局变量只有一个写入者,而有多处读取者,您仅仅关心变量的最新写入值的情况下,您怎么能断定不能使用全局变量呢?
------------------------------------------------------------------------------------------------------------------
Darren:
全局变量在某些情况下是非常好用的。如果我有一些静态数据,这些静态数据必须在多个VI中共享,这种情况下,我会使用全局变量存储这些静态数据。(所谓静态数据就是不需要改变的数据,常量)。最常见的例子是需要给用户提示的文本字符串。如果我有一个非常复杂的GUI,需要在很多地方向用户提示文本信息。我会创建一个全局变量VI,把这些字符串创建为全局变量,并且按照字母顺序排序(通过设置TAB ORDER)。这样我们需要在程序框图中使用全局变量时,直接拖入并选择我们需要的。这种方法可以很容易使我们的应用程序本地化,因为所有的显示字符串集中在一个VI之中,而不是散布在各个VI之中,很容易集中处理。
我另外一次使用全局变量是在一个子面板应用中,因为我的子面板中的VI不需要和主VI交换信息,所以我将子面板VI中的数据写入全局变量,供应用程序其它部分读取。因为不需要同步化以及只有子面板中的VI写入数据,保证了只有一个写入者,这恰恰是全局变量的最佳工作方式。
------------------------------------------------------------------------------------------------------------------
TBOB:
很高兴能看到同人谈及如何恰当地使用全局变量,而不是简单地说完全不要使用全局变量。我们更应该强调如何恰当地使用全局变量,帮助人们了解数据竞争是如何产生的,以及如何避免竞争情况出现。

我大多数使用全局变量时,是把全局变量作为常量来使用的,比如保存一个GPIB的地址。它们一旦创建后就永远不会再次写入更改,这种情况下,绝对不会出现数据竞争的情况。或者在生产消费者模式中,生产者写入全局变量,而消费者读取全局变量。这种情况下,读的时机是非常重要的。我使消费者不断查询全局变量,是否和原来的值发生变化。换句话说,消费者在数据更新之前可能读取了两次,当然并不很理想。
对于局部变量也是如此,总有它们合适使用的场合,但是必须小心可能会导致的问题。教会人们发现问题和解决问题好于仅仅说避免使用它们。
------------------------------------------------------------------------------------------------------------------
Jasonhill:
我也经常看到要求禁止使用全局变量,在合适的条件下,使用全局变量还是非常有用的。但是程序员还是会不自觉地倾向于滥用它们,任何变量(全局变量、局部变量、LV2全局变量)在使用时需要格外小心,“连线”还是最安全的。
我非常讨厌上下或者左右堆积大量的控件,在程序框图中多达20几个层叠顺序结构中,到处散布一些全局变量或者局部变量。
至于你提及的生产者消费者模式,我还是愿意使用队列来完成,使用队列可以使我们不需要考虑读的时机问题。
------------------------------------------------------------------------------------------------------------------
TBOB
在生产者消费者模式中,使用队列(我也倾向于使用队列,而不是变量)同样存在问题。消费者可能运行速度高于消费者,此时可能读回空数据,必须在编程中检查是否是否读回空数据。
------------------------------------------------------------------------------------------------------------------
TST:
在生产消费者模式使用队列时,我愿意使用超时的默认值-1,这意味着消费者在没有数据时不会执行一个循环,也不需要检查超时是否发生了。
------------------------------------------------------------------------------------------------------------------
TITOU:
真是个好题目!
全局变量是魔鬼吗?------我愿意这样回答:不是,只要你遵循了全局变量的工作规则。
我经常建议避免使用全局变量,但是的确在特定的场合,我还是会使用全局变量,因为使用全局变量的确非常方便。
使用但不要滥用------------------------------------------------------------------------------------------------------------------
 ROBERT:
即使在基于文本的编程语言中,采用封装和抽象本身就倾向于不使用全局变量。理想的结构应该是这样的,如果函数需要一个变量,必须从函数的调用者哪里接收这个变量。尽管如此,即使在这样的编程环境中,还是需要有限度的和合理的利用全局变量。正如上面的帖子中指出的那样,一个写入者,多个读取者。亦或需要在整个程序应用,但是不需要改变的场合。
我经常采样下面的方式。在程序启动时,先运行一个配置函数或者“参数设置”函数,此时没有其它的进程工作,数据采集也尚未进行。此时为程序的其它部分创建全局变量是合理的。
------------------------------------------------------------------------------------------------------------------
KEVIN:
我在程序开发时,习惯于在多个消费者情况下使用“通告”。通常情况下,只有一个生产者。但是像全局变量情况,可能会有几个“潜在”的生产者。
对于使用通告,消费者可以进行选择。通告可以不管消费这是否已经消费了先前的数据,随时查询最新的数据,这类似于全局变量。通告也可以一直等待,直至有最新更新的数据,避免不断的轮询数据,加重CPU的负担,这个是全局变量无法实现的。
-----------------------------------------------------------------------------------------------------------------
BEN:
很抱歉没能早点参与这个话题的讨论。我并非求全责备,但是全局变量存在下列主要问题:
1、数据拷贝
2、利用线程
3、对于一个写入者,多个读取者,OK.但是这要求开发者必须确认只有唯一一个写入者。这对一个拥有800多个VI,有些是动态载入的情况下,是很难做到的。
4、灵活性 。如果你使用一个LV2型全局变量,需要的情况下,你可以很安全地添加新的新的写入者。在编写大型应用时,这的确是令人头疼的问题。
5、性能。LV2全局变量可以很容易重用缓存,全局变量不行。
-----------------------------------------------------------------------------------------------------------------
RAY:
我同意大家的看法。我通常用全局变量保持静态数据,比如IP地址。
-----------------------------------------------------------------------------------------------------------------
 
类似的问题在论坛上看到多次了,非常典型,所以汇总几种常见的方法供大家参考。

在实际应用中,经常会遇到多个不同的事件源,需要触发同一事件的问题。最为常见的是程序的菜单项和工具栏的按钮问题。通常情况下,工具栏的按钮对应菜单条的中的某一项,二者的作用是相同的。在C语言编程中,通常为二者注册同一回调函数,这样二者的反应就完全相同了。

在LABVIEW的编程实践中,也会遇到类似的问题,基于LABVIEW事件结构的特殊性,存在几种常见的方法可以解决这个问题。

1、为一个分支静态注册多个事件

这种方法与其它编程语言类似,类似于为多个事件注册同一回调函数。下面我用四个按钮来举例说明。

Picture
显然图中四个按钮的作用是类似的,比如移动图片,包括上下左右四个方向。一般情况下,我们对四个按钮是分别静态注册事件,因此会出现四个事件处理分支,非常繁琐,也容易出现问题。

初学者可能没有注意到一个事件分支可以静态注册多个控件的事件,注册方法如下图所示:

Picture
Picture
通过事件结构中控件的引用可以区分是那个按钮产生了事件。

2、利用值(信号)属性节点转发事件

如果我们为每个按钮单独注册了事件,可以在一个按钮事件分支中处理所有按钮的响应代码,通过值(信号)属性节点,可以编程实现触发事件,如下图所示:

Picture

3、不同事件分支调用同一VI

这种方法使用比较简便,对需要相同功能的事件分支,调用同一函数(VI),因为非常简单,就不举例了。

4、采用生产消费者模式(事件)

使用基于事件的生产消费者模式后,对于不同的事件分支,产生相同的功能就非常容易了,无非是向队列入队相同的数据而已。LABVIEW提供了生产消费者模式的模版,如下图所示:

Picture
但是一般都要根据自己的需要,重新封装队列,比如下面的例子:

Picture
上面介绍了几种常用的方法,如果我们使用了生产消费者模式,结合事件结构,我们就有了自己的消息处理中心,这种方式与WINDOWS的消息机制非常类似,使事件结构的处理更为灵活,重点推荐这种方式。

 
作为一种面向工程应用的编程语言,LV提供了非常丰富的时间操作函数。8.X后又提出了新的有关时间的数据类型,时间标识(TIMESTAMP)。时间标识早期通常翻译成“时间戳”,实际上是一种改进型的数值控件,从时间标识控件所在的控件选板就可以初步判断出,时间标识就是特殊的数值控件。

Picture
一、时间标识的内存映射
要想真正了解一种数据类型,首要的问题是要搞清楚该数据类型在内存中或者文件中是如何存储的。我们知道数值型控件可以选择控件所包含的数据类型,比如双精度浮点数、整型数、32位整型数、16位整型数等等。
时间标识控件是不允许选择它所包含的数据类型的,这说明时间标识所包含的数据类型是固定的。从帮助文件中,我们可以找到时间标识在内存中的存储方式--映射。

LabVIEW将时间标识保存为一个含四个整数的簇,其中前两个带符号整数(64位二进制)表示自1904年1月1日周五凌晨[01-01-1904 00:00:00]以来无时区影响的所有秒数。后两个不带符号整数(64位二进制)表示小数秒部分。

Picture
LV利用16个字节(128位)表示时间信息,其中前8个字节(64位)由两个I32构成,表示从0时刻开始经历过的秒数。后面8个字节为U64数据,表示秒的小数部分。

二、时间标识与双精度数之间的相互转换
在时间标识出现以前,经常用双精度数表示从0时刻经历过的秒数。我们知道双精度数所占的内存空间也是8个字节,与时间标识相同。但是时间标识实际上是定点数,它的小数点位置是确定的,因此实际上双精度数表示时间与时间标识相比,不如时间标识精确。
采用数值转换函数就可以实现时间标识和双精度数之间的相互转换,如下图所示:

Picture
三、强制转换时间标识至数值
既然我们已经知道了时间标识在内存中的存储方式,我们自然就可以通过强制转换函数分解出时间标识的两个组成部分,秒和秒的小数部分。
因为时间标识占有8个字节的内存空间,并分成前后各4个字节,因此可以创建一个簇或者数组来表示它,下面的例子创建一个簇,包含3个元素。前两个元素表示秒的整数部分,第3个元素表示秒的小数部分。
 
Picture
从上图可以看出,秒的整数部分非常明显。小数部分就不容易分辨出来了,这要非常了解浮点数的二进制表示方式才可以。
小数部分虽然有4个字节(64位)构成,但是实际上只有前8位(一个字节)会发生变化,这也说明了“获取时间日期(秒)”函数所能达到的最高精度是1/256秒。
小数部分的HEX进制0X60是如何对应0.375秒的那?
0X60用二进制表示为:0110 0000,所以它表示的小数为:
0*0.5+1*0.25+1*0.125+。。。=0.375

四、什么是0时刻
当我们从控件选板中建一个新的控件时,控件此时的值为默认值。数值型控件的默认值如果没有重新设置,一般是0。时间标识作为特殊的数值控件,当我们新创建时,时间标识控件的默认值是什么?

Picture
从上图可以看出,新创建的时间标识的默认值实际上也是0.从控件上看,数值0对应的时间为0,日期不显示。下面我们修改当前时间控件,把时间修改成8点零1秒。

Picture
从上图可以看出,LABVIEW中的时间都是从1904年1月1日8点开始计算的,这个时间就是0时刻。我们计算时间时实际上计算的是从此刻开始经历的秒数。经历过的秒数可以是负数,表示此时计算的时间在0时刻之前。
回过头来再看看帮助文件是如何定义的:

LabVIEW将时间标识保存为一个含四个整数的簇,其中前两个带符号整数(64位二进制)表示自1904年1月1日周五凌晨[01-01-1904 00:00:00]以来无时区影响的所有秒数。后两个不带符号整数(64位二进制)表示小数秒部分。
帮助文件中定义的和我们图中演示的是不同的,原因在于我们使用的时间是包括时区的,世界各地的所在的时区是不同的,因此定义的0时刻是无时区影响的。
 
 
无时区影响的时间日期,LABVIEW中称作“通用时间”。由于我们所使用的计算机操作系统中的时间日期是包括时区的,所以只有非常特别的场合才会涉及到“通用时间”,绝大多数场合谈到的都是“绝对时间”和“相对时间”


五、绝对时间和相对时间
数值控件、时间标识、格式化字符串、波形图、波形图表等控件均可以设置为时间格式,设置为时间时要求我们选择“绝对时间”或者“相对时间”。所以我们有必要搞清楚什么是“绝对时间”和“相对时间”。
从0时刻开始计时的时间称作绝对时间,我们通常说的“系统时间和日期”指的就是绝对时间。“相对时间”在其它编程语言中,称作“时间跨度”,相对时间并不关注开始计时的时刻,相对时间关注的是两个绝对时间的差值,比如秒数百米赛跑用时几秒,用的就是相对时间。
绝对时间可以用年、月、星期、日、时、分、秒、小数秒来表示,但是相对时间是用日、时、分、秒、小数秒来表示的,这与我们日常的习惯是有很大区别的。
在日常生活中,我们经常说两年过去,几个月过去了,这样的说法实际上是不精确的。因为每年的天数是不固定的,每个月的天数也是不固定的,因此无法用年和月表示准确的相对时间。
只有星期、天、时、分、秒是准确的,每小时为3600秒在任何情况下都是成立的,而年、月与秒之间是无法直接转换的。

 
单选按钮在LABVIEW中是非常特别的控件,在8.0以前版本并不存在,而单选按钮是WINDOWS最为常用的控件之一,我们很难理解为什么早期LABVIEW不提供单选按钮控件,可能是由于控件内部设计机制的问题。

虽然现在LABVIEW提供了单选按钮控件,LABVIEW中的单选控件也是非常另类的,首先它所在的控件选板就很奇怪,下面重点讨论几个有关单选按钮的特性。

一、单选按钮是布尔类型控件还是数值类型控件
WINDOWS下的单选按钮是互斥按钮,从按钮的角度看,是由布尔控件组成的。但是整体来看,它又代表了数值。
LABVIEW中的单选按钮位于布尔控件选板中,如下图所示:
Picture
按钮为2值控件,只有真假两个值,单选按钮却代表多个值,它的归类够另类的吧。

二、从单选按钮使用拉看,单选按钮更像是枚举控件
从一下几个特性来看,单选按钮更像是枚举控件。
1、单选按钮与枚举控件一样,都是代表几个固定的数值
2、枚举控件与单选控件的接线端子非常相像,如下图所示
 
Picture
3、单选按钮可以直接进行加减运算,并具有越界自动回卷的功能,与枚举控件相同。
当我们选择最后一个单选按钮,进行加一操作后,自动回卷到第一个单选按钮。
4、通过单选按钮创建的常量为枚举类型常量,3、4如下图所示:
 
Picture
三、单选按钮可以作为条件结构的选择器。
同枚举控件一样,单选按钮可以作为条件结构的选择器,如下图所示:
 
Picture
四、单选按钮可以设置为“不选”

其它编程工具提供的单选按钮的值一般从0开始,0表示选择的是第一个,1表示选择第二个,依次类推。LABVIEW中,默认情况下创建的单选按钮也是如此,0表示选择的是第一个。
请注意LABVIEW中单选按钮的一个重要特点,LABVIEW允许不选取任何单选按钮。通过单选按钮的快捷菜单可以设置。
 
Picture
当选择“允许不选”时,0表示所有按钮均不选,1表示选择第一个按钮。LABVIEW中的单选按钮也可以选择机械动作。机械动作有两种选择,“单击时转换”和“释放时转换”。

五、单选按钮的个性化
VC中如果要对单选按钮中的按钮进行特色话,必须采用“自画”的方法,非常复杂,LABVIEW中的单选按钮特色话就简单多了,可以使用任何外形的按钮控件,而且不要求各个按钮必须一致。
 
Picture
Picture