您的位置 首页 软件

LabWindows™/CVI中的多线程技能

1.进行多线程编程的原因在程序中使用多线程技术的原因主要有四个。最常见的原因是多个任务进行分割,这些任务中的一个或多个是对时间要求…

1. 进行多线程编程的原因

在程序中运用多线程技能的原因首要有四个。最常见的原因是多个使命进行切割,这些使命中的一个或多个是对时刻要求严厉的而且易被其他使命的运转所干与。例如,进行数据收集并显现用户界面的程序就很适宜运用多线程技能完结。在这种类型的程序中,数据收集是时刻要求严厉的使命,它很或许被用户界面的使命打断。在LabWindows/CVI程序中运用单线程办法时,程序员或许需求从数据收集缓冲区读出数据并将它们显现到用户界面的曲线上,然后处理事情对用户界面进行更新。当用户在界面上进行操作(如在图表上拖动光标)时,线程将继续处理用户界面事情而不能回来到数据收集使命,这将导致数据收集缓冲区的溢出。而在LabWindows/CVI程序中运用多线程技能时,程序员能够将数据收集操作放在一个线程中,而将用户界面处理放在另一个线程中。这样,在用户对界面进行操作时,操作体系将进行线程切换,为数据收集线程供给完结使命所需的时刻。

在程序中运用多线程技能的第二个原因是程序中或许需求一起进行低速的输入/输出操作。例如,运用仪器来测验电路板的程序将从多线程技能中取得显着的功用进步。在LabWindows/CVI程序中运用单线程技能时,程序员需求从串口发送数据,初始化电路板。,程序需求等候电路板完结操作之后,再去初始化测验仪器。有必要要等候测验仪器完结初始化之后,再进行丈量,。在LabWindows/CVI程序中运用多线程技能时,你能够运用另一个线程来初始化测验仪器。这样,在等候电路板初始化的一起等候仪器初始化。低速的输入/输出操作一起进行,减少了等候所需求的时刻总开支。

在程序中运用多线程技能的第三个原因是凭借多处理器核算机来进步功用。核算机上的每个处理器能够都履行一个线程。这样,在单处理器核算机上,操作体系仅仅使多个线程看起来是一起履行的,而在多处理器核算机上,操作体系才是真实意义上一起履行多个线程的。例如,进行数据收集、将数据写入磁盘、剖析数据而且在用户界面上显现剖析数据,这样的程序很或许经过多线程技能和多处理器核算机运转得到功用进步。将数据写到磁盘上和剖析用于显现的数据是能够一起履行的使命。

在程序中运用多线程技能的第四个原因是在多个环境中一起履行特定的使命。例如,程序员能够在运用程序中运用多线程技能在测验舱进行并行化测验。运用单线程技能,运用程序需求动态分配空间来保存每个舱中的测验成果。运用程序需求手动维护每个记载及其对应的测验舱的联系。而运用多线程技能,运用程序能够创立独立的线程来处理每个测验舱。然后,运用程序能够运用线程局部变量为每个线程创立测验成果记载。测验舱与成果记载间的联系是主动维护的,使运用程序代码得以简化。

2. 挑选适宜的操作体系

微软公司的Windows 9x系列操作体系不支撑多处理器核算机。所以,你有必要在多处理器核算机上运转Windows Vista/XP/2000/NT 4.0体系来享用多处理器带来的优点。而且,即便在单处理器核算机上,多线程程序在Windows Vista/XP/2000/NT 4.0上的功用也比在Windows 9x上好。这要归功于Windows Vista/XP/2000/NT 4.0体系有着更为高效的线程切换技能。可是,这种功用上的差别在大都多线程程序中表现得并不是十分显着。

关于程序开发,特别是编写和调试多线程程序而言,Windows Vista/XP/2000/NT 4.0系列操作体系比Windows 9x系列更为安稳,当运转操作体系代码的线程被暂停或中止的时分,操作体系的一些部分有或许出于不良状况中。这种状况使得Windows 9x操作体系溃散的几率远远高于Windows Vista/XP/2000/NT 4.0体系的几率。所以,NI公司引荐用户运用运转Windows Vista/XP/2000/NT 4.0操作体系的核算机来开发多线程程序。

3. LabWindows/CVI中的多线程技能简介

NI LabWindows/CVI软件自二十世纪九十年代中期诞生之日起就支撑多线程运用程序的创立。现在,跟着多核CPU的广泛遍及,用户能够运用LabWindows/CVI来充分运用多线程技能的优势。

与Windows SDK threading API(Windows 软件开发工具包线程API)比较,LabWindows/CVI的多线程库供给了以下多个功用优化:

  • Thread pools协助用户将函数调度到独立的线程中履行。Thread pools处理线程缓存来最小化与创立和毁掉线程相关的开支。
  • Thread-safe queues对线程间的数据传递进行了笼统。一个线程能够在另一个线程向行列写入数据的一起,从行列中读取数据。
  • Thread-safe variables高效地将临界代码段和恣意的数据类型结合在一起。用户能够调用简略的函数来获取临界代码段,设定变量值,然后开释临界代码段。
  • Thread locks供给了共同的API并在必要时主动挑选适宜的机制来简化临界代码段和互斥量的运用。例如,假如需求在进程间同享互斥锁,或许线程需求在等候锁的时分处理音讯,LabWindows/CVI会主动运用互斥量。临界代码段运用在其它场合中,由于它愈加高效。
  • Thread-local variables为每个线程供给变量实例。操作体系对每个进程可用的线程局部变量的数量进行了约束。LabWindows/CVI在完结过程中对线程局部变量进行了加强,程序中的一切线程局部变量只运用一个进程变量。

能够在Utility Library»Multithreading下的LabWindows/CVI库函数树状图中找到一切的多线程函数。

4. 在LabWindows/CVI的辅佐线程中运转代码

单线程程序中的线程被称为主线程。在用户奉告操作体系开端履行特定的程序时,操作体系将创立主线程。在多线程程序中,除了主线程外,程序还奉告操作体系创立其他的线程。这些线程被称为辅佐线程。主线程和辅佐线程的首要差异在于它们开端履行的方位。操作体系从main或许WinMain函数开端履行主线程,而由开发人员来指定辅佐线程开端履行的方位。

在典型的LabWindows/CVI多线程程序中,开发者运用主线程来创立、显现和运转用户界面,而运用辅佐线程来进行其它时刻要求严厉的操作,如数据收集等。LabWindows/CVI供给了两种在辅佐进程中运转代码的高档机制。这两种机制是线程池(thread pools)和异步定时器。线程池适宜于履行若干次的或许一个循环内履行的使命。而异步定时器适宜于定时进行的使命。

运用线程池

为了运用LabWindows/CVI的线程池在辅佐线程中履行代码,需求调用Utility Library中的CmtScheduleThreadPoolFunction函数。将需求在辅佐线程中运转的函数称号传递进来。线程池将这个函数调度到某个线程中履行。依据装备状况和当时的状况,线程池或许会创立新的线程来履行这个函数、也或许会运用已存在的闲暇进程履行函数或许会等候一个活泼的线程变为闲暇然后运用该线程履行预订的函数。传递给CmtScheduleThreadPoolFunction的函数被称为线程函数。线程池中的线程函数能够挑选恣意的称号,可是有必要遵从以下原型:

int CVICALLBACK ThreadFunction (void *functionData);

下面的代码显现了怎么运用CmtScheduleThreadPoolFunction函数在辅佐进程中履行一个数据收集的线程。

int CVICALLBACK DataAcqThreadFunction (void *functionData);
int main(int argc, char *argv[])
{
int panelHandle;
int functionId;

if (InitCVIRTE (0, argv, 0) == 0)
return -1; /* out of memory */
if ((panelHandle = LoadPanel(0, “DAQDisplay.uir”, PANEL)) < 0)
return -1;
DisplayPanel (panelHandle);

CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, DataAcqThreadFunction, NULL, &functionId);
RunUserInterface ();
DiscardPanel (panelHandle);
CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
return 0;
}
int CVICALLBACK DataAcqThreadFunction (void *functionData)
{
while (!quit) {
Acquire(. . .);
Analyze(. . .);
}
return 0;
}

在前面的代码中,主线程调用了CmtScheduleThreadPoolFunction函数,使线程池创立了一个新的线程来运转DataAcqThreadFunction线程函数。主线程从CmtScheduleThreadPoolFunction函数回来,而无须等候DataAcqThreadFunction函数完结。在辅佐线程中的DataAcqThreadFunction函数与主线程中的调用是一起履行的。

CmtScheduleThreadPoolFunction函数的第一个参数表明用于进行函数调度的线程池。LabWindows/CVI的Utility Library中包含了内建的默许线程池。传递常数DEFAULT_THREAD_POOL_HANDLE表明用户期望运用默许的线程池。可是用户不能对默许线程池的行为进行自界说。用户能够调用CmtNewThreadPool函数来创立自界说的线程池。CmtNewThreadPool函数回来一个线程池句柄,这个句柄将作为第一个参数传递给CmtScheduleThreadPoolFunction函数。程序员需求调用CmtDiscardThreadPool函数来开释由CmtNewThreadPool函数创立的线程池资源。

CmtScheduleThreadPoolFunction函数中的最终一个参数回来一个标识符,用于在后面的函数调用中引证被调度的函数。调用CmtWaitForThreadPoolFunctionCompletion函数使得主线程等候线程池函数完毕后再退出。假如主线程在辅佐线程完结之前退出,那么或许会形成辅佐线程不能正确地整理分配到的资源。这些辅佐线程运用的库也不会被正确的开释掉。

运用异步定时器

为了运用LabWindows/CVI的异步定时器在辅佐线程中运转代码,需求调用Toolslib中的NewAsyncTimer函数。需求向函数传递在辅佐线程中运转的函数称号和函数履行的时刻距离。传递给NewAsyncTimer的函数被称为异步定时器回调函数。异步定时器仪器驱动程序会依照用户指定的周期调用异步定时器回调函数。异步定时器回调函数的称号是恣意的,可是有必要遵从下面的原型:

int CVICALLBACKFunctionName(int reserved, int timerId, int event, void *callbackData, int eventData1, int eventData2);

由于LabWindows/CVI的异步定时器仪器驱动运用Windows多媒体定时器来完结异步定时器回调函数,所以用户可指定的最小距离是随运用的核算机不同而改动的。假如用户指定了一个比体系可用的最大分辨率还小的时刻距离,那么或许会产生不行预知的行为。不行预知的行为一般产生在设定的时刻距离小于10ms时。一起,异步定时器仪器驱动运用一个多媒体定时器线程来运转单个程序中注册的一切异步定时器回调函数。所以,假如用户期望程序并行地履行多个函数,那么NI公司引荐运用LabWindows/CVI Utility Library中的线程池函数来替代异步定时器函数。

5. 维护数据

在运用辅佐线程的时分,程序员需求处理的一个十分要害的问题是数据维护。在多个线程一起进行拜访时,程序需求对大局变量、静态局部变量和动态分配的变量进行维护。不这样做会导致间歇性的逻辑过错产生,而且很难发现。LabWindows/CVI供给了各种高档机制协助用户对遭到并发拜访的数据进行维护。维护数据时,一个重要的考虑便是防止死锁。

假如一个变量被多个线程拜访,那么它有必要被维护,以保证它的值牢靠。例如下面一个比如,一个多线程程序在多个线程中对大局整型counter变量的值进行累加。

count = count + 1;

这段代码依照下列CPU指令次序履行的:

1.将变量值移入处理器的寄存器中

2.添加寄存器中的变量值

3.把寄存器中的变量值写回count变量

由于操作体系或许在线程运转过程中的恣意时刻打断线程,所以履行这些指令的两个线程或许依照如下的次序进行(假定count初始值为5):

线程1:将count变量的值移到寄存器中。(count=5,寄存器=5),然后切换到线程2(count=5,寄存器不知道)。

线程2:将count变量的值移到寄存器中(count=5,寄存器=5)。

线程2: 添加寄存器中的值(count=5,寄存器=6)。

线程2: 将寄存器中的值写回count变量(count=6,寄存器=6),然后切换回线程1.(count=6,寄存器=5)。

线程1: 添加寄存器的值。(count=6,寄存器=6)。

线程1: 将寄存器中的值写回count变量(count= 6, register = 6)。

由于线程1在添加变量值并将其写回之前被打断,所以变量count的值被设为6而不是7。操作体系为体系中地每一个线程的寄存器都保存了副本。即便编写了count++这样的代码,用户仍是会遇到相同的问题,由于处理器会将代码依照多条指令履行。留意,特定的时序状况导致了这个过错。这就意味着程序或许正确运转1000次,而只要一次毛病。经历奉告咱们,有着数据维护不妥问题的多线程程序在测验的过程中一般是正确的,可是一到客户装置并运转它们时,就会产生过错。

需求维护的数据类型

只要程序中的多个线程能够拜访到的数据是需求维护的。大局变量、静态局部变量和动态分配内存坐落一般的内存空间中,程序中的一切线程都能够拜访它们。多个线程对内存空间中存储的这些类型的数据进行并发拜访时,有必要加以维护。函数参数和非静态局部变量坐落仓库上。操作体系为每个线程分配独立的仓库。因而,每个线程都具有参数和非静态局部变量的独立副本,所以它们不需求为并发拜访进行维护。下面的代码显现了有必要为并发拜访而维护的数据类型。

int globalArray[1000];// Must be protected
static staticGlobalArray[500];// Must be protected
int globalInt;// Must be protected

void foo (int i)// i does NOT need to be protected
{
int localInt;// Does NOT need to be protected
int localArray[1000];// Does NOT need to be protected
int *dynamicallyAllocdArray;// Must be protected
static int staticLocalArray[1000];// Must be protected

dynamicallyAllocdArray = malloc (1000 * sizeof (int));
}

怎么维护数据

一般说来,在多线程程序中保存数据需求将保存数据的变量与操作体系的线程锁目标相关起来。在读取或许设定变量值的时分,需求首要调用操作体系API函数来获取操作体系的线程锁目标。在读取或设定好变量值后,需求将线程锁目标开释掉。在一个特定的时刻内,操作体系只答应一个线程取得特定的线程锁目标。一旦线程调用操作体系API函数企图获取另一个线程正在持有的线程锁目标,那么企图获取线程锁目标的线程回在操作体系API获取函数中等候,直到具有线程锁目标的线程将它开释掉后才回来。企图获取其它线程持有的线程锁目标的线程被称为堵塞线程。LabWindows/CVI Utility Library供给了三种维护数据的机制:线程锁、线程安全变量和线程安全行列。

线程锁对操作体系供给的简略的线程锁目标进行了封装。在三种状况下,你或许要运用到线程锁。假如有一段需求拜访多个同享数据变量的代码,那么在运转代码前需求取得线程锁,而在代码运转后开释线程锁。与对每段数据都进行维护比较,这个办法的优点是代码更为简略,而且不简略犯错。缺陷是减低了功用,由于程序中的线程持有线程锁的时刻或许会比实际需求的时刻长,这会形成其它线程为取得线程锁而堵塞(等候)的时刻变长。运用线程锁的另一种状况是需求对拜访非线程安全的第三方库函数时进行维护。例如,有一个非线程安全的DLL用于操控硬件设备而你需求在多个线程中调用这个DLL,那么能够在线程中调用DLL前创立需求取得的线程锁。第三种状况是,你需求运用线程锁来维护多个程序间同享的资源。同享内存便是这样一种资源。

线程安全变量技能将操作体系的线程锁目标和需求维护的数据结合起来。与运用线程锁来维护一段数据比较,这种办法更为简略而且不简略犯错。你有必要运用线程安全变量来维护一切类型的数据,包含结构体类型。线程安全变量比线程锁更不简略犯错,是由于用户需求调用Utility Library API函数来拜访数据。而API函数获取操作体系的线程锁目标,防止用户不小心在未获取OS线程锁目标的状况下对数据进行拜访的过错。线程安全变量技能比线程锁更简略,由于用户只需求运用一个变量(线程安全变量句柄),而线程锁技能则需求运用两个变量(线程锁句柄和需求维护的数据自身)。

线程安全行列是一种在线程间进行安全的数组数据传递的机制。在程序中有一个线程生成数组数据而别的一个线程对数组数据进行处理时,需求运用线程安全行列。这类程序的一个比如便是在一个线程中收集数据,而在另一个线程中剖析数据或许将数据显现在LabWindows/CVI的用户界面上。与一个数组类型的线程安全变量比较,线程安全行列有着如下的优势:

  • 线程安全行列在其内部运用了一种锁战略,一个线程能够从行列读取数据而一起另一个线程向行列中写入数据(例如,读取和写入线程不会相互堵塞)。
  • 用户能够为根据事情的拜访装备线程安全行列。用户能够注册一个读取回调函数,在行列中有必定数量的数据可用时,调用这个函数,而且/或许注册一个写入回调函数,在行列中有必定的空间可用时,调用这个函数。
  • 用户能够对线程安全行列进行装备,使得在数据添加而空间已满时,行列能够主动成长。

线程锁技能

在程序初始化的时分,调用CmtNewLock函数来为每个需求维护的数据调集创立线程锁。这个函数回来一个句柄,用户能够运用它在后续的函数调用中指定线程锁。在拜访由锁维护的数据和代码前,线程有必要调用CmtGetLock函数来获取线程锁。在拜访数据后,线程有必要调用CmtReleaseLock函数来开释线程锁。在同一个线程中,能够屡次调用CmtGetLock(不会对后续调用产生堵塞),可是用户每一次调用CmtGetLock都需求调用一次CmtReleaseLock来开释。在程序退出时,调用CmtDiscardLock函数来开释线程锁资源。下面的代码演示了怎么运用LabWindows/CVI Utility Library中的线程锁来维护大局变量。

int lock;
int count;

int main (int argc, char *argv[])
{
int functionId;
CmtNewLock (NULL, 0, &lock);
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
CmtGetLock (lock);
count++;
CmtReleaseLock (lock);
CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
CmtDiscardLock (lock);
}
int CVICALLBACK ThreadFunction (void *functionData)
{
CmtGetLock(lock);
count++;
CmtReleaseLock(lock);
return 0;
}

线程安全变量

线程安全变量技能将数据和操作体系线程锁目标结合成为一个全体。这个办法防止了多线程编程中一个常见的过错:程序员在拜访变量时往往忘掉首要去取得锁。这种办法还使得在函数间传递维护的数据变得简略,由于只需求传递线程安全变量句柄而不需求既传递线程锁句柄又要传递维护的变量。LabWindows/CVI Utility Library API中包含了几种用于创立和拜访线程安全变量的函数。运用这些函数能够创立任何类型的线程安全变量。由于,传递到函数中的参数在类型上是通用的,而且不供给类型安全。一般,你不会直接调用LabWindows/CVI Utility Library中的线程安全变量函数。

LabWindows/CVI Utility Library中的头文件中包含了一些宏,它们供给了合作Utility Library函数运用的类型安全的封装函数。除了供给类型安全,这些宏还协助防止了多线程编程中的其它两个常见过错。这些过错是在拜访数据后忘掉开释锁目标,或许是在前面没有获取锁目标时企图开释锁目标。运用DefineThreadSafeScalarVar和DefineThreadSafeArrayVar宏来创立线程安全变量和类型安全的函数供运用和拜访。假如需求从多个源文件中拜访线程安全变量,请在include(.h)文件中运用DeclareThreadSafeScalarVar或许DeclareThreadSafeArrayVar宏来创立拜访函数的声明。DefineThreadSafeScalarVar(datatype,VarName,maxGetPointerNestingLevel)宏创立以下拜访函数:

int InitializeVarName (void);
void UninitializeVarName (void);
datatype *GetPointerToVarName (void);
void ReleasePointerToVarName (void);
void SetVarName (datatype val);
datatype GetVarName (void);

留意事项:这些宏运用传递进来的第二个参数(在这个比如中为VarName)作为标识来为线程安全变量创立自界说的拜访函数称号。

留意事项:maxGetPointerNestingLevel参数将在“检测GetPointerToVarName不匹配调用”一节中进跋涉一步评论。

在第一次拜访线程安全变量前首要调用一次(只在一个线程里)InitializeVarName函数。在程序间断前调用UninitializeVarName函数。假如需求对变量当时的值进行更改(如,添加一个整数的值),那么请调用GetPointerToVarName函数,更改动量值,然后调用ReleasePointerToVarName函数。在同一个线程中,能够屡次调用GetPointerToVarName函数(在后续的调用中不会产生堵塞),可是有必要调用相同次数的ReleasePointerToVarName函数与GetPointerToVarName一一对应。假如在相同的线程中,调用了ReleasePointerToVarName函数,而前面没有与之相匹配的GetPointerToVarName调用,那么ReleasePointerToVarName将会陈述一个run-time error过错。

假如需求对变量值进行设定而不需求考虑其当时值,那么请调用SetVarName函数。假如需求取得变量的当时值,请调用GetVarName函数。需求了解的一点是,在GetVarName从内存中读出变量值后而在其将变量值回来给你前,变量的值是有或许改动的。

下面的代码显现了怎么运用线程安全变量作为前面比如中说到的计数变量。

DefineThreadSafeScalarVar (int, Count, 0);
int CVICALLBACK ThreadFunction (void *functionData);

int main (int argc, char *argv[])
{
int functionId;
int *countPtr;

InitializeCount();
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
countPtr = GetPointerToCount();
(*countPtr)++;
ReleasePointerToCount();
CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
UninitializeCount();
return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
int *countPtr;

countPtr = GetPointerToCount();
(*countPtr)++;
ReleasePointerToCount();
return 0;
}

运用数组作为线程安全变量

DefineThreadSafeArrayVar宏与DefineThreadSafeScalarVar宏类似,可是它还需求一个额定的参数来指定数组中元素的个数。一起,与DefineThreadSafeScalarVar不同,DefineThreadSafeArrayVar没有界说GetVarName和SetVarName函数。下面的声明界说了有10个整数的线程安全数组。
DefineThreadSafeArrayVar (int, Array, 10, 0);

将多个变量结合成单个线程安全变量

假如有多个互相相关的变量,那么有必要制止两个线程一起对这些变量进行修正。例如,有一个数组和记载数组中有用数据数意图count变量。假如一个线程需求删去数组中的数据,那么在另一个线程拜访数据前,有必要对数组和变量count值进行更新。尽管能够运用单个LabWindows/CVI Utility Library线程锁来对这两种数据的拜访维护,可是更安全的做法是界说一个结构体,然后运用这个结构体作为线程安全变量。下面的比如显现了怎么运用线程安全变量来安全地向数组中填加一个数据。

typedef struct {
int data[500];
int count;
} BufType;

DefineThreadSafeVar(BufType, SafeBuf);

void StoreValue(int val)
{
BufType *safeBufPtr;
safeBufPtr = GetPointerToSafeBuf();
safeBufPtr->data[safeBufPtr->count] = val;
safeBufPtr->count++;
ReleasePointerToSafeBuf();
}

检测对GetPointerToVarName的不匹配调用

能够经过DefineThreadSafeScalarVar和DefineThreadSafeArrayVar的最终一个参数(maxGetPointerNestingLevel),来指定最大数意图嵌套调用。一般能够把这个参数设为0,这样GetPointerToVarName在检测到同一线程中对GetPointerToVarName的两次接连调用而中心没有对ReleasePointerToVarName进行调用时,就会报出一个运转过错。例如,下面的代码在第2次履行的时分会报出run-time error的过错,由于它忘掉了调用ReleasePointerToCount函数。

int IncrementCount (void)
{
int *countPtr;

countPtr = GetPointerToCount(); /* run-time error on second execution */
(*countPtr)++;
/* Missing call to ReleasePointerToCount here */
return 0;
}

假如代码中有必要对GetPointerToVarName进行嵌套调用时,那么可将maxGetPointerNestingLevel参数设为一个大于零的整数。例如,下面的代码将maxGetPointerNestingLevel参数设定为1,因而它答应对GetPointerToVarName进行一级嵌套调用。

DefineThreadSafeScalarVar (int, Count, 1);
int Count (void)
{
int *countPtr;
countPtr = GetPointerToCount();
(*countPtr)++;
DoSomethingElse(); /* calls GetPointerToCount */
ReleasePointerToCount ();
return 0;
}
void DoSomethingElse(void)
{
int *countPtr;
countPtr = GetPointerToCount(); /* nested call to GetPointerToCount */
… /* do something with countPtr */
ReleasePointerToCount ();
}

假如不知道GetPointerToVarName的最大嵌套等级,那么请传递TSV_ALLOW_UNLIMITED_NESTING来禁用对GetPointerToVarName函数的不匹配调用查看。

线程安全行列

运用LabWindows/CVI Utility Library的线程安全行列,能够在线程间安全地传递数据。当需求用一个线程来收集数据而用另一个线程来处理数据时,这种技能十分有用。线程安全行列在其内部处理一切的数据确认。一般说来,运用程序中的辅佐线程获取数据,而主线程在数据可用时读取数据然后剖析并/或显现数据。下面的代码显现了线程怎么运用线程安全行列将数据传递到别的一个线程。在数据可用时,主线程运用回调函数来读取数据。

int queue;
int panelHandle;

int main (int argc, char *argv[])
{
if (InitCVIRTE (0, argv, 0) == 0)
return -1; /* out of memory */
if ((panelHandle = LoadPanel(0, “DAQDisplay.uir”, PANEL)) < 0)
return -1;
/* create queue that holds 1000 doubles and grows if needed */
CmtNewTSQ(1000, sizeof(double), OPT_TSQ_DYNAMIC_SIZE, &queue);
CmtInstallTSQCallback (queue, EVENT_TSQ_ITEMS_IN_QUEUE, 500, QueueReadCallback, 0, CmtGetCurrentThreadID(), NULL);
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, DataAcqThreadFunction, NULL, NULL);
DisplayPanel (panelHandle);
RunUserInterface();
. . .
return 0;
}
void CVICALLBACK QueueReadCallback (int queueHandle, unsigned int event, int value, void *callbackData)
{
double data[500];
CmtReadTSQData (queue, data, 500, TSQ_INFINITE_TIMEOUT, 0);
}

6. 防止死锁

当两个线程一起等候对方持有的线程确认目标时,代码就不能继续运转了。这种状况被称为死锁。假如用户界面线程产生死锁,那么它就不能响运用户的输入。用户有必要非正常地完毕程序。下面的比如解说了死锁是怎么产生的。

线程1:调用函数来获取线程锁A(线程1:无线程锁,线程2:无线程锁)。

线程1:从获取线程锁的函数回来(线程1:持有线程锁A,线程2:无线程锁)。

切换到线程2:(线程1:持有线程锁A,线程2:无线程锁)。

线程2:调用函数来获取线程锁B(线程1:持有线程锁A,线程2:无线程锁)。

线程2:从获取线程锁的函数回来(线程1:持有线程锁A,线程2:持有线程锁B)。

线程2:调用函数来获取线程锁A(线程1:持有线程锁A,线程2:持有线程锁B)。

线程2:由于线程1持有线程锁A而被堵塞(线程1:持有线程锁A,线程2:持有线程锁B)。

切换到线程1:调用函数来获取线程锁B(线程1:持有线程锁A,线程2:持有线程锁B)。

线程1:调用函数来获取线程锁B(线程1:持有线程锁A,线程2:持有线程锁B)。

线程1:由于线程2持有线程锁A而被堵塞(线程1:持有线程锁A,线程2:持有线程锁B)。

与不对数据进行维护时产生的过错类似,由于程序运转的状况不同导致线程切换的时序不同,死锁过错间歇性地产生。例如,假如直到线程1持有线程锁A和B后才切换到线程2,那么线程1就能够完结作业而开释掉这些线程锁,让线程2在晚些时分获取到。就像上面所说的那样,死锁现象只要在线程一起获取线程锁时才会产生。所以你能够运用简略的规矩来防止这种死锁。当需求获取多个线程锁目标时,程序中的每个线程都需求依照相同的次序来获取线程锁目标。下面的LabWindows/CVI Utility Library函数获取线程锁目标,而且回来时并不开释这些目标。

  • CmtGetLock
  • CmtGetTSQReadPtr
  • CmtGetTSQWritePtr

留意事项:一般说来,不需求直接调用CmtGetTSVPtr函数。它是经过DeclareThreadSafeVariable宏创立的GetPtrToVarName函数调用的。因而,关于调用的GetPtrToVarName函数需求将它作为线程锁目标获取函数来对待,应该留意死锁维护的问题。
The following Windows SDK functions can acquire thread-locking objects without releasing them before returning.Note:This is not a comprehensive list.

下面的Windows SDK函数能够获取线程锁目标但在回来时并不开释这些目标。留意,这不是完好的列表。

  • EnterCriticalSection
  • CreateMutex
  • CreateSemaphore
  • SignalObjectAndWait
  • WaitForSingleObject
  • MsgWaitForMultipleObjectsEx

7. 监督和操控辅佐线程

在把一个函数调度到独立的线程中运转时,需求对被调度函数的运转状况进行监督。为了取得被调度函数的运转状况,调用CmtGetThreadPoolFunctionAttribute来取得ATTR_TP_FUNCTION_EXECUTION_STATUS特点的值。也能够注册一个回调函数,线程池调用之后当即运转被调度的函数和/或开端运转后当即由线程池调用。假如需求注册这样的回调函数,有必要运用CmtScheduleThreadFunctionAdv来对函数进行调度。

一般说来,辅佐进程需求在主线程完毕程序前完结。假如主线程在辅佐线程完结之前完毕,那么辅佐线程将不能够将分配到的资源整理掉。一起,或许导致这些辅佐线程所运用的库函数也不能被正确铲除。

能够调用CmtWaitForThreadPoolFunctionCompletion函数来安全地等候辅佐线程完毕运转,然后答应主线程完毕。

在一些比如中,辅佐线程函数有必要继续完结一些作业直到主线程让它中止下来。在这类状况下,辅佐线程一般在while循环中完结使命。while循环的条件是主线程中设定的整数变量,当主线程需求奉告辅佐线程中止运转时,将其设为非零整数。下面的代码显现了怎么运用while循环来操控辅佐线程何时完毕履行。

volatile int quit = 0;

int main (int argc, char *argv[])
{
int functionId;
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
// This would typically be done inside a user interface
// Quit button callback.
quit = 1;
CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
while (!quit) {
. . .
}
return 0;
}

留意事项:假如运用volatile要害字,这段代码在经过优化的编译器(如Microsoft Visual C++)后功用是正常的。优化的编译器确认while循环中的代码不会修正quit变量的值。因而,作为优化,编译器或许只运用quit变量在while循环条件中的初始值。运用volatile要害字是奉告编译器另一个线程或许会改动quit变量的值。这样,编译器在每次循环运转时都运用更新往后的quit变量值。

有些时分,当主线程进行其他使命的时分需求暂停辅佐线程的运转。假如你暂停正在运转操作体系代码的线程,或许会使得操作体系处于不合法状况。因而,在需求暂停的线程中需求一直调用Windows SDK的SuspendThreadfunction函数。这样,能够保证线程在运转要害代码时不被暂停。在另一个线程中调用Windows SDK的ResumeThreadfunction是安全的。下面的代码展现了怎么运用它们。

volatile int quit = 0;

int main (int argc, char *argv[])
{
int functionId;
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
// This would typically be done inside a user interface
// Quit button callback.
quit = 1;
CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
while (!quit) {
. . .
}
return 0;
}

8. 进程和线程优先级

在Windows操作体系中,能够指定每个进程和线程作业的相对重要性(被称为优先级)。假如给予进程或线程以较高的优先级,那么它们将取得比优先级较低的线程更好的优先挑选。这意味着当多个线程需求运转的时分,具有最高优先级的线程首要运转。

Windows将优先级分类。同一进程中的一切线程具有相同的优先级类别。同一进程中的每个线程都有着与进程优先级类别相关的优先级。能够调用Windows SDK中的SetProcessPriorityClass函数来设定体系中线程的优先级。

NI公司不引荐用户将线程的优先级设为实时优先级,除非只在很短时刻内这样做。当进程被设为实时优先级时,它运转时体系中止会被堵塞。这会形成鼠标、键盘、硬盘及其它至关重要的体系特性不能作业,并很或许形成体系被确认。

假如你是运用CmtScheduleThreadFunctionAdv函数来将函数调度到线程池中运转,那么还能够指定履行所调度函数的线程的优先级。线程池在运转被调度的函数前会改动线程优先级。在函数完毕运转后,线程池会将线程优先级康复到本来的优先级。可运用CmtScheduleThreadFunctionAdv函数来在默许的和自界说的线程池中指定线程的优先级。

在创立自界说的LabWindows/CVI Utility Library线程池(调用CmtNewThreadPool函数)时,能够设定池中各线程的默许优先级。

9. 音讯处理

每个创立了窗口的线程有必要对Windows音讯进行处理以防止体系确认。用户界面库中的RunUserInterfacefunction函数包含了处理LabWindows/CVI用户界面事情和Windows音讯的循环。用户界面库中的GetUserEvent和ProcessSystemEventsfunctions函数在每次被调用时对Windows音讯进行处理。假如下列状况中的之一被满意,那么程序中的每个线程都需求调用GetUserEventor和ProcessSystemEventsregularly函数来处理Windows音讯。

  • 线程创立了窗口但没有调用RunUserInterface函数。
  • 线程创立了窗口并调用了RunUserInterface函数,可是在回来到RunUserInterface循环前需求运转的回调函数占用了很多时刻(多于几百毫秒)。

可是,在代码中的某些当地不适宜用于处理Windows音讯。在LabWindows/CVI的用户界面线程中调用了GetUserEvent、ProcessSystemEvents或RunUserInterface函数时,线程能够调用一个用户界面回调函数。假如在用户界面回调函数中调用这些函数之一,那么线程将调用别的一个回调函数。除非需求这样做,不然这种事情将产生不行预知的行为。

Utility Library中的多线程函数会形成线程在循环中等候,答应你指定是否在等候线程中对音讯进行处理。例如,CmtWaitForThreadPoolFunctionCompletion函数中有个Option参数,能够运用它来指定处理Windows音讯的等候线程。

有的时分,线程对窗口的创立不是那么清楚明了的。用户界面库函数如LoadPanel、CreatePanel和FileSelectPopup等都创立了用于显现和丢掉的窗口。这些函数还为每个调用它们的线程创立了躲藏的窗口。在毁掉可见的窗口时,这个躲藏的窗口并没有被毁掉。除了这些用户界面库函数外,各种其它的LabWindows/CVI库函数和Windows API函数创立了躲藏的布景窗口。为了防止体系的确认,有必要在线程中对运用这两种办法创立的窗口的Windows音讯进行处理。

10. 运用线程局部变量

线程局部变量与大局变量类似,能够在恣意线程中对它们进行拜访。可是,大局变量关于一切线程只保存一个值,而线程局部变量为每个拜访的线程保存一个独立的值。当程序中需求一起在多个上下文中进行相同的使命,而其间每个上下文都对应一个独立的线程时,一般需求运用到线程局部变量。例如,你编写了一个并行的测验程序,其间的每个线程处理一个待测单元,那么你或许需求运用线程局部变量来保存每个单元的特定信息(例如序列号)。

Windows API供给了用于创立和拜访线程局部变量的机制,可是该机制对每个进程中可用的线程局部变量的数目进行了约束。LabWindows/CVI Utility Library中的线程局部变量函数没有这种约束。下面的代码展现了怎么创立和拜访一个保存了整数的线程局部变量。

volatile int quit = 0;
volatile int suspend = 0;
int main (int argc, char *argv[])
{
int functionId;
HANDLE threadHandle;
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, NULL, &functionId);
. . .
// This would typically be done in response to user input or a
// change in program state.
suspend = 1;
. . .
CmtGetThreadPoolFunctionAttribute (DEFAULT_THREAD_POOL_HANDLE, functionId, ATTR_TP_FUNCTION_THREAD_HANDLE, &threadHandle);
ResumeThread (threadHandle);
. . .
return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
while (!quit) {
if (suspend) {
SuspendThread (GetCurrentThread ());
suspend = 0;
}
. . .
}
return 0;
}

int CVICALLBACK ThreadFunction (void *functionData);
int tlvHandle;
int gSecondaryThreadTlvVal;

int main (int argc, char *argv[])
{
int functionId;
int *tlvPtr;

if (InitCVIRTE (0, argv, 0) == 0)
return -1; /* out of memory */
CmtNewThreadLocalVar (sizeof(int), NULL, NULL, NULL, &tlvHandle);
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, 0, &functionId);
CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
CmtGetThreadLocalVar (tlvHandle, &tlvPtr);
(*tlvPtr)++;
// Assert that tlvPtr has been incremented only once in this thread.
assert (*tlvPtr == gSecondaryThreadTlvVal);
CmtDiscardThreadLocalVar (tlvHandle);
return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
int *tlvPtr;

CmtGetThreadLocalVar (tlvHandle, &tlvPtr);
(*tlvPtr)++;
gSecondaryThreadTlvVal = *tlvPtr;
return 0;
}

11. 在线程局部变量中存储动态分配的数据

假如你运用线程局部变量来存储动态分配到的资源,那么你需求开释掉分配的资源的每一个复制。也便是说,你需求开释掉每个线程平分配到的资源复制。运用LabWindows/CVI的线程局部变量,你能够指定用于毁掉线程局部变量的回调函数。当你毁掉线程局部变量时,每个拜访过变量的线程都会调用指定的回调函数。下面的代码展现了怎么创立和拜访保存了动态分配的字符串的线程局部变量。

int CVICALLBACK ThreadFunction (void *functionData);
void CVICALLBACK StringCreate (char *strToCreate);
void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID);
int tlvHandle;
volatile int quit = 0;
volatile int secondStrCreated = 0;

int main (int argc, char *argv[])
{
int functionId;

if (InitCVIRTE (0, argv, 0) == 0)
return -1; /* out of memory */
CmtNewThreadLocalVar (sizeof(char *), NULL, StringDiscard, NULL, &tlvHandle);
CmtScheduleThreadPoolFunction (DEFAULT_THREAD_POOL_HANDLE, ThreadFunction, “Secondary Thread”, &functionId);
StringCreate (“Main Thread”);
while (!secondStrCreated){
ProcessSystemEvents ();
Delay (0.001);
}
CmtDiscardThreadLocalVar (tlvHandle);
quit = 1;
CmtWaitForThreadPoolFunctionCompletion (DEFAULT_THREAD_POOL_HANDLE, functionId, 0);
return 0;
}
int CVICALLBACK ThreadFunction (void *functionData)
{
char **sString;

// Create thread local string variable
StringCreate ((char *)functionData);

// Get thread local string and print it
CmtGetThreadLocalVar (tlvHandle, &sString);
printf (“Thread local string: %s\n”, *sString);

secondStrCreated = 1;

while (!quit)
{
ProcessSystemEvents ();
Delay (0.001);
}

return 0;
}
void CVICALLBACK StringCreate (char *strToCreate)
{
char **tlvStringPtr;
CmtGetThreadLocalVar (tlvHandle, &tlvStringPtr);
*tlvStringPtr = malloc (strlen (strToCreate) + 1);
strcpy (*tlvStringPtr, strToCreate);
}
void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID)
{
char *str = *(char **)threadLocalPtr;
free (str);
}

一些分配的资源有必要在分配到它们的线程中开释。这些资源被称为具有线程相关度。例如,面板有必要在创立它的线程中毁掉掉。在调用CmtDiscardThreadLocalVar时,Utility Library在线程中调用被称为CmtDiscardThreadLocalVar的线程局部变量毁掉回调函数。Utility Library为每一个拜访过该变量的线程调用一次毁掉回调函数。它将threadID参数传递给毁掉回调函数,这个参数指定了调用毁掉回调函数的线程的ID号。你能够运用这个线程ID来确认是否能够直接开释掉具有线程相关度的资源仍是有必要在正确的线程中调用Toolslib中的PostDeferredCallToThreadAndWait函数来开释资源。下面的代码显现了怎么更改前面的比如以在分配字符串的线程中将它们开释掉。

void CVICALLBACK StringDiscard (void *threadLocalPtr, int event, void *callbackData, unsigned int threadID)
{
char *str = *(char **)threadLocalPtr;

if (threadID == CmtGetCurrentThreadID ())
free (str);
else
PostDeferredCallToThreadAndWait (free, str, threadID, POST_CALL_WAIT_TIMEOUT_INFINITE);
}

12. 在独立线程中运转的回调函数

运用LabWindows/CVI中的一些库,你能够在体系创立的线程中接回收调函数。由于这些库会主动创立履行回调函数的线程,所以你不需求创立线程或许将函数调度到独自的线程中履行。在程序中,你依然需求对这些线程和其它线程间同享的数据进行维护。这些回调函数的完结一般被称为是异步事情。

LabWindows/CVI的GPIB/GPIB 488.2库中,能够调用ibnotify来注册事情产生时GPIB/GPIB 488.2库调用的回调函数。你能够为每一个电路板或器材指定一个回调函数。能够为事情指定调用的回调函数。GPIB/GPIB 488.2库会创立用于履行回调函数的线程。

在LabWindows/CVI的虚拟仪器软件构架 (VISA) 库中,你能够调用viInstallHandler函数来注册多个事情句柄(回调函数)用于在特定的ViSession中接纳VISA事情(I/O完结、服务恳求等等)类型。VISA库一般创立独立的线程来履行回调函数。VISA或许会对一个进程中的一切回调函数运用同一个线程,或许对每个ViSession运用独自的线程。你需求为某个指定的事情类型调用viEnableEvent函数以奉告VISA库调用已注册的事情句柄。

在LabWindows/CVI VXI库中,每个中止或回调函数类型都有自己的回调注册和使能函数。例如,为了接纳NI-VXI中止,你有必要调用SetVXIintHandler和EnableVXIint函数。VXI库运用自己创立的独立线程来履行回调函数。关于同一进程中一切的回调函数,VXI都运用相同的线程。

13. 为线程设定首选的处理器

能够运用渠道SDK中的SetThreadIdealProcessor函数来指定履行某一线程的处理器。这个函数的第一个参数是线程句柄。第二个参数是以零为索引开始的处理器。能够调用LabWindows/CVI Utility Library中的CmtGetThreadPoolFunctionAttribute函数,运用ATTR_TP_FUNCTION_THREAD_HANDLE特点来获取线程池线程的句柄。能够调用LabWindows/CVI Utility Library中的CmtGetNumberOfProcessors函数来经过程序来确认运转该程序的核算机上处理器的数量。

能够运用渠道SDK中的SetProcessAffinityMask函数来指定答应履行你的程序的处理器。能够运用渠道SDK中的SetThreadAffinityMask函数来指定答应履行程序中特定线程的处理器。传递到SetThreadAffinityMask中的mask变量有必要是传递到SetProcessAffinityMask中的mask变量的子集。

声明:本文内容来自网络转载或用户投稿,文章版权归原作者和原出处所有。文中观点,不代表本站立场。若有侵权请联系本站删除(kf@86ic.com)https://www.86ic.net/qianrushi/ruanjian/222421.html

为您推荐

联系我们

联系我们

在线咨询: QQ交谈

邮箱: kf@86ic.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部