LabWindows™/CVI中的线技术

概览


多核编程基础系列白皮书


多任务、多线程和多处理这些术语经常被交替地使用,但是它们在本质上是不同的概念。多任务是指操作系统具有在任务间快速切换使得这些任务看起来是在同步执行的能力。在一个抢占式多任务系统中,应用程序可以随时被暂停。使用多线程技术,应用程序可以把它的任务分配到单独的线程中执行。在多线程程序中,操作系统让一个线程的代码执行一段时间(被称为时间片)后,会切换到另外的线程继续运行。暂停某个线程的运行而开始执行另一个线程的行为被称为线程切换。通常情况下,操作系统进行线程切换的速度非常快,令用户觉得有多个线程在同时运行一样。多处理指的是在一台计算机上使用多个处理器。在对称式多处理(SMP)系统中,操作系统自动使用计算机上所有的处理器来执行所有准备运行的线程。借助于多处理的能力,多线程应用程序可以同时执行多个线程,在更短的时间内完成更多的任务。

单线程应用程序移植到多核处理器上运行不会获得性能上的改进,这是因为它们只能在其中一个处理器上运行,而不能像多线程应用程序那样在所有的处理器上同时运行。而且单线程应用程序需要承受操作系统在处理器间切换所需要的开销。为了在多线程操作系统和/或多处理器计算机上获得最优异的性能,我们必须使用多线程技术来编写应用程序。

内容

进行线原因

在程序中使用多线程技术的原因主要有四个。最常见的原因是多个任务进行分割,这些任务中的一个或多个是对时间要求严格的而且易被其他任务的运行所干涉。例如,进行数据采集并显示用户界面的程序就很适合使用多线程技术实现。在这种类型的程序中,数据采集是时间要求严格的任务,它很可能被用户界面的任务打断。在LabWindows/CVI程序中使用单线程方法时,程序员可能需要从数据采集缓冲区读出数据并将它们显示到用户界面的曲线上,然后处理事件对用户界面进行更新。当用户在界面上进行操作(如在图表上拖动光标)时,线程将继续处理用户界面事件而不能返回到数据采集任务,这将导致数据采集缓冲区的溢出。而在LabWindows/CVI程序中使用多线程技术时,程序员可以将数据采集操作放在一个线程中,而将用户界面处理放在另一个线程中。这样,在用户对界面进行操作时,操作系统将进行线程切换,为数据采集线程提供完成任务所需的时间。

在程序中使用多线程技术的第二个原因是程序中可能需要同时进行低速的输入/输出操作。例如,使用仪器来测试电路板的程序将从多线程技术中获得显著的性能提升。在LabWindows/CVI程序中使用单线程技术时,程序员需要从串口发送数据,初始化电路板。,程序需要等待电路板完成操作之后,再去初始化测试仪器。必须要等待测试仪器完成初始化之后,再进行测量,。在LabWindows/CVI程序中使用多线程技术时,你可以使用另一个线程来初始化测试仪器。这样,在等待电路板初始化的同时等待仪器初始化。低速的输入/输出操作同时进行,减少了等待所需要的时间总开销。

在程序中使用多线程技术的第三个原因是借助多处理器计算机来提高性能。计算机上的每个处理器可以都执行一个线程。这样,在单处理器计算机上,操作系统只是使多个线程看起来是同时执行的,而在多处理器计算机上,操作系统才是真正意义上同时执行多个线程的。例如,进行数据采集、将数据写入磁盘、分析数据并且在用户界面上显示分析数据,这样的程序很可能通过多线程技术和多处理器计算机运行得到性能提升。将数据写到磁盘上和分析用于显示的数据是可以同时执行的任务。

在程序中使用多线程技术的第四个原因是在多个环境中同时执行特定的任务。例如,程序员可以在应用程序中利用多线程技术在测试舱进行并行化测试。使用单线程技术,应用程序需要动态分配空间来保存每个舱中的测试结果。应用程序需要手动维护每个记录及其对应的测试舱的关系。而使用多线程技术,应用程序可以创建独立的线程来处理每个测试舱。然后,应用程序可以使用线程局部变量为每个线程创建测试结果记录。测试舱与结果记录间的关系是自动维护的,使应用程序代码得以简化。

选择合适操作系统

微软公司的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操作系统的计算机来开发多线程程序。

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库函数树状图中找到所有的多线程函数。

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 CVICALLBACK FunctionName (int reserved, int timerId, int event, void *callbackData, int eventData1, int eventData2);

由于LabWindows/CVI的异步定时器仪器驱动使用Windows多媒体定时器来实现异步定时器回调函数,所以用户可指定的最小间隔是随使用的计算机不同而变化的。如果用户指定了一个比系统可用的最大分辨率还小的时间间隔,那么可能会产生不可预知的行为。不可预知的行为通常发生在设定的时间间隔小于10ms时。同时,异步定时器仪器驱动使用一个多媒体定时器线程来运行单个程序中注册的所有异步定时器回调函数。所以,如果用户希望程序并行地执行多个函数,那么NI公司推荐使用LabWindows/CVI Utility Library中的线程池函数来代替异步定时器函数。

保护数据

在使用辅助线程的时候,程序员需要解决的一个非常关键的问题是数据保护。在多个线程同时进行访问时,程序需要对全局变量、静态局部变量和动态分配的变量进行保护。不这样做会导致间歇性的逻辑错误发生,而且很难发现。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进行调用时,就会报出一个运行错误。例如,下面的代码在第二次执行的时候会报出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);
}

 避免死锁

当两个线程同时等待对方持有的线程锁定对象时,代码就不能继续运行了。这种状况被称为死锁。如果用户界面线程发生死锁,那么它就不能响应用户的输入。用户必须非正常地结束程序。下面的例子解释了死锁是如何发生的。

线程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

 监视控制辅助线程

在把一个函数调度到独立的线程中运行时,需要对被调度函数的运行状态进行监视。为了获得被调度函数的运行状态,调用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;

 进程线优先级

在Windows操作系统中,可以指定每个进程和线程工作的相对重要性(被称为优先级)。如果给予进程或线程以较高的优先级,那么它们将获得比优先级较低的线程更好的优先选择。这意味着当多个线程需要运行的时候,具有最高优先级的线程首先运行。

Windows将优先级分类。同一进程中的所有线程拥有相同的优先级类别。同一进程中的每个线程都有着与进程优先级类别相关的优先级。可以调用Windows SDK中的SetProcessPriorityClass函数来设定系统中线程的优先级。

NI公司不推荐用户将线程的优先级设为实时优先级,除非只在很短时间内这样做。当进程被设为实时优先级时,它运行时系统中断会被阻塞。这会造成鼠标、键盘、硬盘及其它至关重要的系统特性不能工作,并很可能造成系统被锁定。

如果你是使用CmtScheduleThreadFunctionAdv函数来将函数调度到线程池中运行,那么还可以指定执行所调度函数的线程的优先级。线程池在运行被调度的函数前会改变线程优先级。在函数结束运行后,线程池会将线程优先级恢复到原来的优先级。可使用CmtScheduleThreadFunctionAdv函数来在默认的和自定义的线程池中指定线程的优先级。

 

在创建自定义的LabWindows/CVI Utility Library线程池(调用CmtNewThreadPool函数)时,可以设定池中各线程的默认优先级。

 消息处理

每个创建了窗口的线程必须对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消息进行处理。

 使用线局部变量

线程局部变量与全局变量相似,可以在任意线程中对它们进行访问。但是,全局变量对于所有线程只保存一个值,而线程局部变量为每个访问的线程保存一个独立的值。当程序中需要同时在多个上下文中进行相同的任务,而其中每个上下文都对应一个独立的线程时,通常需要使用到线程局部变量。例如,你编写了一个并行的测试程序,其中的每个线程处理一个待测单元,那么你可能需要使用线程局部变量来保存每个单元的特定信息(例如序列号)。

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;
}

 在线局部变量存储动态分配数据

如果你使用线程局部变量来存储动态分配到的资源,那么你需要释放掉分配的资源的每一个拷贝。也就是说,你需要释放掉每个线程中分配到的资源拷贝。使用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);

 在独立线运行函数

使用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都使用相同的线程。

 为线设定处理器

可以使用平台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变量的子集。

这些函数只有程序在装有Microsoft Windows XP/2000/NT 4.0系统的多处理器计算机上运行才有效果。Microsoft Windows 9x系列的操作系统不支持多处理器计算机。

Was this information helpful?

Yes

No