大家知道,LV的一个优点在于框图是并行执行,CVI作为一个C语言编程环境继承了其它开发环境的基础,是一种顺序执行策略。虽然C或C++中存在跳转、循环语句但请大家注意:看上去它们在打破顺序结构,但其实程序仍然在按照我们预先定义好的顺序在执行。这意味着程序永远都是执行完上一个语句后执行下一个语句,只不过可能并非下一行。这种执行结构优点十分明显——代码的执行过程和时间是可事先预测的。但也因此带来了问题。我们来看我刚学CVI时写的程序。
在公司每周都要复制测试设备中的数据库记录文件,每次我不得不从每个计算机复制文件,删除文件,更换成空的文件,完成备份工作。本来用批处理写最好,但每台计算机的数据库位置不一样,需要输入参数,为此我用CVI写了个我认为应该十分简单:
CopyFile (sourcepath, Targetpath);
DeleteFile(sourcepath);
但当我测试时发现执行程序时会出现界面假死的情况。在复制完文件之前,对程序做不了任何操作,相信大家应该也碰到过这种情况。主要原因要从CPU运行机制说起。
大家都知道,CPU在每个单位时间内一次只能进行一项运算,而我们平时在Windows中的那些所谓“多任务”的操作,其实就是让CPU根据优先权轮流来执行打开的程序,Windows这个操作系统说穿了只是“欺骗”CPU的一个软件,它时刻都让CPU感觉到只在运行一个程序,之所以当我们打开很多进程时,每个进程都能同时运行,都是Windows在后台帮的忙,让CPU轮流运行的,由于运行速度极快,你根本没办法感觉出现在正在运行什么进程或者现在哪些进程停止了。而当你运行SuperPi时,哪怕你关闭了所有能关闭的进程后,其实在后台还有很多进程在运行,这些进程你是无法关闭的,最简单的比喻就是你动一下鼠标这样的简单动作,在CPU那里也要花一定的时间来处理你的这个操作。
在默认情况下CVI生成的EXE运行时在一个进程空间中。这意味着代码的执行都在一个CPU时间片断中,所有的任务都要排队等CPU来处理,这还要考虑CPU不是一直在执行CVI程序的进程,因此在事件函数中一但执行了才时间处理函数会使这个函数占用整个CVI进程CPU占用。在这时对其它任何控件的操作都要排队等,用户看来就是操作没反应。如果函数执行时间十分极端(如读取串口数据或数据库所有行读取)在几分钟内都不能完成时,不仅事件响应 要排队,要知道WINDOWS消息响应也要排队。在当中有几个十分重要的消息——WM_PAINT(要求一个窗口重画自己)WM_CLOSE(当一个窗口或应用程序要关闭时发送一个信号)都是低优先级的。这时它们只能排队,如果你移动或把其它程序显示在CVI程序上,系统会要重新更新CVI程序界面显示,而代码不能马上执行,大家会看到程序就像死机一样,所有的标题栏以下都是一片白。同样退出指令也没希望执行,所以大家在日常运行程序时如果有一个程序死循环,就没法让它退出,如果这个程序正好在占用100%CPU,你就等好了,不RESET就不能干任何事。
解决的方法就是采用多线程技术。原因大家应该明白了,让它分离主程序的运行,单独让CPU处理。
图:一个进程(process)含有两个线程(threads)的运行
使用该进程的全局变量和系统资源。操作系统给每个线程分配不同的CPU时间片,在某一个时刻,CPU只执行一个时间片内的线程,多个时间片中的相应线程在CPU内轮流执行,由于每个时间片时间很短,所以对用户来说,仿佛各个线程在计算机中是并行处理的,如上图所示。操作系统是根据线程的优先级来安排CPU的时间,优先级高的线程优先运行,优先级低的线程则继续等待。这让我们的主程序不会停止事件的处理,而耗时的函数也在不断的由CPU执行。使用户用起来一切正常。
传说的双核大家都用上了,大家不要误解双核不等于多线程。双核意味着能同时执行两个CPU片断,片断的单位是线程,但CVI默认生成的是一个线程,所以所有的处理仍然要挤在一起,只是在任务管理器中看到50%CPU占用,另一个能处理其它事。这就是为什么双核的死机情况会好很多,一个废了还有一个顶着。但对一个程序来说就是100%死机。
下面我们改下代码:
#include
#include
#include
#include
#include "1.h"
static int panelHandle;
int ID;
int CVICALLBACK ThreadFunction (void*functionData);
int main (int argc, char *argv[])
{
if (InitCVIRTE (0, argv, 0) == 0)
return -1;
if ((panelHandle = LoadPanel (0, "1.uir", PANEL)) <0)
return -1;
DisplayPanel (panelHandle);
RunUserInterface ();
DiscardPanel(panelHandle);
return 0;
}
int CVICALLBACK Exit (int panel, int event, void*callbackData,
int eventData1, int eventData2)
{
switch (event)
{
case EVENT_GOT_FOCUS:
break;
case EVENT_LOST_FOCUS:
break;
case EVENT_CLOSE:
CmtWaitForThreadPoolFunctionCompletion(DEFAULT_THREAD_POOL_HANDLE, ID,OPT_TP_PROCESS_EVENTS_WHILE_WAITING);
CmtReleaseThreadPoolFunctionID(DEFAULT_THREAD_POOL_HANDLE,ID);
//等待函数执行完后结束新的线程
QuitUserInterface (0);
break;
}
return 0;
}
int CVICALLBACK Copy (int panel, int control, intevent,
void *callbackData, int eventData1, int eventData2)
{
switch (event)
{
case EVENT_COMMIT:
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE , &ThreadFunction, NULL,&ID); //用多线程执行该函数
break;
}
return 0;
}
int CVICALLBACK Updatetime (int panel, int control, intevent,
void *callbackData, int eventData1, int eventData2)
{
int year,month,day;
int hours,minutes,seconds;
char Datetime[30];
switch (event)
{
case EVENT_TIMER_TICK:
//生成时间
GetSystemDate (&month, &day,&year);
GetSystemTime (&hours, &minutes,&seconds);
Fmt(Datetime,"%s<今天是:%d[w4p0]-%d[w2p0]-%d[w2p0]%d[w2p0]:%d[w2p0]:%d[w2p0]",year,month,day,hours,minutes,seconds);
SetCtrlVal(panelHandle,PANEL_STRING,Datetime);
break;
}
return 0;
}
int CVICALLBACK ThreadFunction (void*functionData)
{
CopyFile("e:\adsl_be.mdb","f:");
return 0;
}
此时在执行COPY时STRING控件的时间仍然在更新,证明多线程的作用。
这里主要讲解原理,代码中使用的CVI多线程语句十分简单。
多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。CVI也提供了相关的处理死锁等语句,以后再做介绍。