|
发表于 2005-12-29 05:41:29
|
显示全部楼层
<DIV>多线程请教我想把采集、显示、存盘分别放入三个线程,采集是主线程,其他放入辅助线程。 <BR>每当有100点是刷一次屏,每当到达缓冲区一半时存一次盘,假设我的Buffer设 <BR>的时10000,我该如何设另外两个线程?Daq函数用哪个好一点?回复:多线程请教把采集放在次线程就可以了,保存数据也可以在采集线程中同时完成,主线程用来显示波形,当 <BR>你的采样速率不同时,缓冲的长度要改变一下可能会好一些回复:多线程请教在多线程中最好用环形缓冲的模式采集数据,便于两个线程传递数据<BR>需要了解你的采集机制。通常采集卡定时向上位机传送数据块,上位机通过中断信号接收数据块,如果存盘过程在采集线程中完成,即采集一次存盘一次,必须保证两次采集之间有足够的时间用于存盘操作,以保证采集数据不至丢失;如果采样频率很高,存盘数据量很大的情况下可考虑使用单独的存盘线程及相应的大缓存。采集的核心是数据不能丢失,如果必要可以进行抽点显示,以保证采集、存盘线程拥有足够的CPU时间片。</DIV>
<DIV></DIV>
<DIV></DIV>
<DIV></DIV>
<DIV>低版CVI没有专门的多线程函数库,但可以使用WINDOWS SDK中的相关函数,多线程函数原形在WINBASE.H中,导入库为KERNEL32.LIB。在创建多线程时只要INCLUDE<WINDOWS.H>即可,与SEED资源用法相同。</DIV>
<DIV></DIV>
<DIV></DIV>
<DIV></DIV>
<DIV>以下建立线程及暂停、继续执行线程、设置线程优先级函数名:<BR>CreateThread<BR>SuspendThread<BR>ResumeThread<BR>SetThreadPriority(可设置从最低-15到最高+15的7档优先级)<BR>线程间通讯可查WINDOWS SDK相应资料。</DIV>
<DIV></DIV>
<DIV>主题 CVI5.0下基于win32 API的多线程编程<BR>日期 25 Dec 2003 02:48:10 -0600<BR>来自 "zhoujian29" <<a href="mailto:zhoujian29@yahoo.com" target="_blank" >zhoujian29@yahoo.com</A>> <BR>A</DIV>
<DIV></DIV>
<DIV><1> CVI5.0下基于win32 API的多线程编程,why?<BR><BR>CVI5.5和5.5以上的版本,已经有自己的多线程库(CVI library),并提供两种高级应用:线程池<BR>(threadpool)和异步计时器(asynchronous timer)。相比CVI5.0,5.5的多线程编程简洁大方便于<BR>修改,因此笔者并不推荐初学者(我自己也是初学CVI多线程)用CVI5.0来实现多线程。主要是俺手<BR>中只有CVI5.0,且笔者花了三天来研究,为了给这三天有个小结,也为了用不上CVI5.5以上版本<BR>的穷人也能用CVI5.0来实现多线程并少走些弯路(对于拥有CVI5.5和5.5以上的版本的richer来<BR>说,继续看该文将有浪费时间的危险!可以直接看四海测控论坛上的《LabWindows/CVI多线程程<BR>序设计》),特此把我的一点心得公布于此,由于笔者是硬件工程师出身,自身软件水平有限,以<BR>下内容如有贻笑大方的错误,请高手指点一二。说来也怪,测控界的高手好像并不是很热衷于把<BR>自己掌握的绝技公布出来,所以,看书查资料运行例程是我等愚笨之人的首选。<BR></DIV>
<DIV></DIV>
<DIV><2>有关多线程的基础知识及相关文献<BR><a href="http://www.51cnet.com/" target="_blank" >http://www.51cnet.com</A>上的《Windows平台下的多线程编程》提供了一个用VC++实现Win32<BR>API下<BR>的多线程编程的入门级介绍,其中就提到:<BR>“<BR>Win32 API是Windows操作系统内核与应用程序之间的界面,它将内核提供的功能进行函数包装,<BR>应用程序通过调用相关函数而获得相应的系统功能。为了向应用程序提供多线程功能,Win32<BR>API<BR>函数集中提供了一些处理多线程程序的函数集。直接用Win32 API进行程序设计具有很多优点:<BR>基<BR>于Win32的应用程序执行代码小,运行效率高,但是它要求程序员编写的代码较多,且需要管理所<BR>有系统提供给程序的资源。用Win32 API直接编写程序要求程序员对Windows系统内核有一定的了<BR>解,会占用程序员很多时间对系统资源进行管理,因而程序员的工作效率降低。 <BR>1. 用Win32函数创建和终止线程 <BR>Win32函数库中提供了操作多线程的函数,包括创建线程、终止线程、建立互斥区等。在应用程序<BR>的主线程或者其他活动线程中创建新的线程的函数如下: <BR>HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpThreadAttributes,DWORD <BR>dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD</DIV>
<DIV></DIV>
<DIV>dwCreationFlags,LPDWORD lpThreadId); <BR>如果创建成功则返回线程的句柄,否则返回NULL。创建了新的线程后,该线程就开始启动执行<BR>了。但如果在dwCreationFlags中使用了CREATE_SUSPENDED特性,那么线程并不马上执行,而是先<BR>挂起,等到调用ResumeThread后才开始启动线程,在这个过程中可以调用下面这个函数来设置线<BR>程的优先权: <BR>BOOL SetThreadPriority(HANDLE hThread,int nPriority); <BR>当调用线程的函数返回后,线程自动终止。如果需要在线程的执行过程中终止则可调用函数:</DIV>
<DIV></DIV>
<DIV>VOID ExitThread(DWORD dwExitCode); <BR>如果在线程的外面终止线程,则可调用下面的函数: <BR>BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode); <BR>但应注意: 该函数可能会引起系统不稳定,而且线程所占用的资源也不释放。因此,一般情况<BR>下,建议不要使用该函数。 <BR>如果要终止的线程是进程内的最后一个线程,则线程被终止后相应的进程也应终止。<BR>……<BR>2. 线程的同步 <BR>在线程体内,如果该线程完全独立,与其他线程没有数据存取等资源操作上的冲突,则可按照通<BR>常单线程的方法进行编程。但是,在多线程处理时情况常常不是这样,线程之间经常要同时访问<BR>一些资源。由于对共享资源进行访问引起冲突是不可避免的,为了解决这种线程同步问题,<BR>Win32 API提供了多种同步控制对象来帮助程序员解决共享资源访问冲突。在介绍这些同步对象之<BR>前先介绍一下等待函数,因为所有控制对象的访问控制都要用到这个函数。<BR>……<BR>(4)排斥区对象 <BR>在排斥区中异步执行时,它只能在同一进程的线程之间共享资源处理。虽然此时上面介绍的几种<BR>方法均可使用,但是,使用排斥区的方法则使同步管理的效率更高。 <BR>使用时先定义一个CRITICAL_SECTION结构的排斥区对象,在进程使用之前调用如下函数对对象进<BR>行初始化: <BR>VOID InitializeCriticalSection(LPCRITICAL_SECTION); <BR>当一个线程使用排斥区时,调用函数:EnterCriticalSection或者TryEnterCriticalSection;
<DIV></DIV>
<DIV>当要求占用、退出排斥区时,调用函数LeaveCriticalSection,释放对排斥区对象的占用,供其<BR>他线程使用。 <BR>”<BR>在<a href="http://cuit-2.51.net/index/xuexi/vc/vc_chap/CHAP12_2.HTM" target="_blank" >http://cuit-2.51.net/index/xuexi/vc/vc_chap/CHAP12_2.HTM</A>的《12.2 线程的同步》中提<BR>到:<BR>“<BR>由于同一进程的所有线程共享进程的虚拟地址空间,并且线程的中断是汇编语言级的,所以可能<BR>会发生两个线程同时访问同一个对象(包括全局变量、共享资源、API函数和MFC对象等)的情<BR>况,这有可能导致程序错误。<BR>例如,如果一个线程在未完成对某一大尺寸全局变量的读操作时,另一个线程又对该变量进行了<BR>写操作,那么第一个线程读入的变量值可能是一种修改过程中的不稳定值。<BR>属于不同进程的线程在同时访问同一内存区域或共享资源时,也会存在同样的问题。<BR>因此,在多线程应用程序中,常常需要采取一些措施来同步线程的执行。<BR>……<BR>关键节(Critical Seciton)与mutex的功能类似,但它只能由同一进程中的线程使用。关键节可以<BR>防止共享资源被同时访问。<BR>进程负责为关键节分配内存空间,关键节实际上是一个CRITICAL_SECTION型的变量,它一次只能<BR>被一个线程拥有。在线程使用关键节之前,必须调用InitializeCriticalSection函数将其初始<BR>化。如果线程中有一段关键的代码不希望被别的线程中断,那么可以调用EnterCriticalSection<BR>函数来申请关键节的所有权,在运行完关键代码后再用LeaveCriticalSection函数来释放所有<BR>权。如果在调用EnterCriticalSection时关键节对象已被另一个线程拥有,那么该函数将无限期<BR>等待所有权。<BR>”<BR>在机械工业出版社的《Lab Windows/CVI 逐步深入与开发实例》一书中,也有关于基于API的多线<BR>程的简单描述和例程。这是一本好书,书中提到:一个GRAPH控件,要放大和缩小里面的图,只需<BR>修改该GRAPH的属性:Control Mode=hot,copy original plot data=1,Enable zooming=1;<BR>然后<BR>运行就能看到,鼠标在GRAPH控件上,CTRL键+鼠标左键就能放大该图,CTRL键+鼠标右键就能缩小<BR>该图,CTRL键+Shift键+鼠标左键就能拖动该图。很有用,比我以前是写很多代码来控制X,Y轴比<BR>例来实现简单多了。<BR></DIV>
<DIV></DIV>
<DIV><3> CVI5.0提供的基于API的例程onepanel<BR>另外,CVI5.0提供了一个例程在“C:\cvi\samples\sdk\threads\onepanel”目录下,在这个例程<BR>里,该例程中定义了一个结构体,以及一个结构体数组,一个已经创建了多少线程的整数<BR>typedef struct tag_THREAD_INFO{<BR>int ctrlID; //对应的面板上的控件ID号 <BR>HANDLE hThread; //线程句柄<BR>unsigned int CurrentValue; //线程当前值 <BR>} THREAD_INFO;<BR>volatile THREAD_INFO ArrayOfThreadInfo[MAXTHREADS+1] = {0}; </DIV>
<DIV></DIV>
<DIV>解释一下,我的理解,不一定正确:<BR>程序新建一个线程,就占用一个ArrayOfThreadInfo[],并在程序面板Panel中新生成一个numeric<BR>控件用来显示一个不断加1的整数,相应的,<BR>ArrayOfThreadInfo[].hThread就是该线程的线程句柄,<BR>ArrayOfThreadInfo[].ctrlID就是新生成的numeric控件<BR>ArrayOfThreadInfo[].CurrentValue就是该线程里不断加1的那个整数,一旦线程创建成功,就开<BR>始++,直到这个线程结束为止。<BR><BR>注意,例程里,用到这些API函数:<BR>用CreateThread(...)函数创建新的线程的函数,指向DWORD WINAPI MyThread(LPVOID<BR>item) ,<BR>就是在这个MyThread线程函数里,实现了一个变量的自加1。<BR>InitializeCriticalSection()函数初始化关键节(即CRITICAL_SECTION),<BR>EnterCriticalSection()函数和LeaveCriticalSection()函数之间的代码,就是一个线程中的<BR>不希望被别的线程中断的代码,例程里是ArrayOfThreadInfo[(int)item].CurrentValue++;<BR>表示<BR>当前这个线程的结构体数组变量里的CurrentValue将加1。<BR>DeleteCriticalSection()函数在关闭完所有新建线程后,删除该关键节。<BR>GetExitCodeThread()函数可返回一个当前线程是否结束的标志,如果线程已经结束,<BR>它将返回<BR>一个退出代码,如果还在运行,则返回一个STILL_ACTIVE.<BR>TerminateThread()函数用来在线程函数外部手动结束这个线程,但应注意: 该函数可能会引起<BR>系统不稳定,而且线程所占用的资源也不释放。所以例程中是等了10秒钟如果该线程还不自己了<BR>断,才强行用TerminateThread()结束该线程。<BR>另外,因为在CVI5.0的IDE环境中运行的CVI Libraries不是“thread safe”的,所以在IDE环境<BR>中调试程序上就有点麻烦,甚至线程指向的函数MyThread()不是在主程序文件(onepanel.c)中,<BR>而是在独立的一个子程序文件中(mythread.c),并且,mythread.c不能被IDE debug,只能做<BR>成.obj文件供调用,这意味着设置断点都不行,这主要是因为该mythread.c文件用到了一个特殊<BR>的void CVICALLBACK __RunStateChangeCallback(int runState),来根据当前程序运行状态,做<BR>一些事情。这是CVI5.0比5.5很不爽的例子之一。当然,我试着把文件中:<BR>//-----------------------------------------------------------------------------<BR>// Defines<BR>//-----------------------------------------------------------------------------<BR>#if _CVI_DEBUG_<BR>#error This module must be compiled as an OBJ so that the RunStateChange</DIV>
<DIV></DIV>
<DIV>Callback works <BR><BR>properly inside the CVI IDE<BR>#endif <BR>和void CVICALLBACK __RunStateChangeCallback(int runState)<BR>注释掉,也没有出现多大问题,但是变得不安全了。关于安全,让我们看看该文件的顶端的注释<BR>怎么说:<BR>// Using threads with the CVI 5.x Runtime Engine<BR>// * It is safe to use the CVI Libraries in a standalone EXE or external</DIV>
<DIV></DIV>
<DIV>// compiler/linker<BR>//<BR>// Using threads with the CVI 5.x Environment<BR>// * The CVI Libraries are not thread safe inside the CVI environment(IDE).<BR>They <BR>// are only threadsafe when used in a standalone EXE or external compiler/linker.<BR>//<BR>// * The RunStateChangeCallback routine is required to handle the following<BR>issues:<BR>//<BR>// In the LW/CVI environment, when the main process(your program) is suspended<BR>// at a breakpoint, a thread will continue to run, so we will suspend<BR>each<BR>// thread.<BR>//<BR>// When terminating the application within the CVI IDE, the thread and<BR>any <BR>// objects or handles being used by the program must be released properly.<BR>就是说,如果in a standalone EXE or external compiler/linker,安全的,<BR>如果inside the CVI environment(IDE),我理解的在IDE环境中运行程序,就是菜单"RUN->run</DIV>
<DIV></DIV>
<DIV>project"。这样,问题就来了,我的理解是,如果主程序因断点停下了,那么子程序中的线程就<BR>该同时挂起(Suspend),不然,主程序因断点停下了而线程仍然在自顾自地在跑,那就会乱套。<BR>当主程序重新开跑时,那么子程序文件中的线程就该同时恢复(Resume)起来。当主程序要<BR>QuitUserInterface ()后,如果线程没有结束,还要结束线程。<BR>void CVICALLBACK __RunStateChangeCallback(int runState)这个函数就是干这个的。<BR><BR><3.1>CVI5.5里也有这个一个同名例程,基于CVI5.5的库,所以就没有这么多麻烦事,而且更简洁<BR>易懂。<BR></DIV>
<DIV></DIV>
<DIV><4>实战CVI5.0基于win32 API的多线程编程<BR>(a)<BR>好了,罗嗦这么半天,还是来看看多线程能帮我们干些啥子,这才是最重要的。<BR>用过CVI(甚至是所有编程编译器,如VC++)的人都知道,如果程序陷入一个死循环,我们的<BR>panel就死掉了,按任何键都没有反应,包括按退出键都退不出来。简单的解决方案:在面板上增<BR>加一个“退出死循环”按键,我们可以在这个死循环里加一条函数<BR>int GetUserEvent (int Wait_Mode, int *Panel_or_Menu_Bar_Handle, int <BR>*Control_or_Menu_Item_ID);<BR>然后判断*Control_or_Menu_Item_ID,是否就是我们进入死循环后按的那个退出死循环的按键,<BR>如果是,就执行break,就能退出该死循环。但是,如果这个循环的一个周期是10分钟,那就意味<BR>着最长10分钟以后程序才能再次检查到我们按了那个退出键然后才能退出该循环。GetUserEvent<BR>()函数的另一个缺点是占用了CPU时间,非常明显,这个循环的一个周期会大于10分钟。<BR><BR>(b)<BR>我想讨论的,不是死循环,但同样的,假设面对的是一个10分钟才能完成的一个函数。当然,10<BR>分钟太长了,我的程序里,线程函数10秒钟就结束。我想在外设硬件出现异常的情况下马上中指<BR>这个函数。这时,多线程技术就派上用场了。因为是异常,所以要紧急中止,不然就等着挨老板<BR>狠K,所以就顾不得TerminateThread()可能引起的系统不稳定和线程所占用的资源也不释放的问<BR>题了,大不了windows系统崩溃嘛,只要保住值钱的外设就好。<BR>我的程序是沿用CVI例程修改的,只新建了一个线程,并加了些中文注释。越简单的就越容易被移<BR>植哈。<BR>先看线程函数所在的子程序文件的头文件muthread.h:<BR>有个小问题,我的程序只开一个线程,所以<BR>#define MAXTHREADS 1<BR>另外,增加了一个函数<BR>void do_something_after_thread_exit(int index); <BR>因为我的这个线程不是死循环,10秒钟就退出,所以在线程函数结束它的工作并return之前,要<BR>执行这个收尾工作,CloseHandle(线程句柄),并把ArrayOfThreadInfo[]的值全部初始化。<BR>看别人的程序,最好养成先看.h文件的习惯。<BR><BR>(c)<BR>该程序的功能是:用Delay(2)延迟2秒钟后让ArrayOfThreadInfo[(int)item].CurrentValue++;并<BR>显示在面板上的一个numeric控件里,CurrentValue从0开始加,加到5,该线程结束,将耗时10<BR>秒。<BR>面板PANEL上有三个键,分别是“Creat一个线程”,“强制Stop线程”,和“Exit退出该程<BR>序”。<BR>面板上已经有一个NUMERIC控件,让ArrayOfThreadInfo[0].ctrlID=PANEL_NUMERIC就可以用来显<BR>示ArrayOfThreadInfo[].CurrentValue的当前值了。</DIV>
<DIV></DIV>
<DIV>(d)<BR>有趣的是,面板上还有一个时钟,这是CVI5.0例程就有的,每0.25秒把ArrayOfThreadInfo<BR>[].CurrentValue的当前值显示到面板上的个NUMERIC控件里。注意到时钟事件函数里<BR>if !InStandaloneExecutable () 语句没有?只有当我们的这个程序是在IDE环境中运行该程序<BR>时,条件为真,就刷新显示。如果我们是在“资源管理器”里直接跑的EXE文件,条件为假,就不<BR>是由该时钟事件来刷新NUMERIC了,而是线程函数里的一句话来刷新NUMERIC了。也许你有些胡涂<BR>了,为什么要分这么清楚呢?我估计是这样的,这是CVI的一个特性决定的:在CVI的一个函数里<BR>面,一般来说,只有当该函数完成后,诸如SetCtrlVale()这样正对面板控件的函数才能起效。程<BR>序中的注释是这样的:<BR></DIV></DIV> |
|