LabWindows™/CVI 中的多线程技术
发布日期: 四月 18, 2011
概览
多核编程基础系列 白皮书
多任务、多线程 和多处理这些术语经 常被交替地使用,但 是它们在本质上是不 同的概念。多任务是 指操作系统具有在任 务间快速切换使得这 些任务看起来是在同 步执行的能力。在一
个抢占式多任务系统 中,应用程序可以随 时被暂停。使用多线 程技术,应用程序可 以把它的任务分配到 单独的线程中执行。 在多线程程序中,操 作系统让一个线程的 代码执行一段时间
(被称为时间片) 后,会切换到另外的 线程继续运行。暂停 某个线程的运行而开 始执行另一个线程的 行为被称为线程切 换。通常情况下,操 作系统进行线程切换 的速度非常快,令用
户觉得有多个线程在 同时运行一样。多处 理指的是在一台计算 机上使用多个处理 器。在对称式多处理 (SMP)系统中, 操作系统自动使用计 算机上所有的处理器 来执行所有准备运行
的线程。借助于多处 理的能力,多线程应 用程序可以同时执行 多个线程,在更短的 时间内完成更多的任 务。
单线程应用程序移 植到多核处理器上运 行不会获得性能上的 改进,这是因为它们 只能在其中一个处理 器上运行,而不能像 多线程应用程序那样 在所有的处理器上同 时运行。而且单线程
应用程序需要承受操 作系统在处理器间切 换所需要的开销。为 了在多线程操作系统 和/或多处理器计算 机上获得最优异的性 能,我们必须使用多 线程技术来编写应用 程序。
目录
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
进行多线程编程的原 因
选择合适的操作系统
LabWindows/CVI 中的多线程技术简介
在 LabWindows/CVI 的辅助线程中运行代 码
保护数据
避免死锁
监视和控制辅助线 程
进程和线程优先级
消息处理
使用线程局部变量
在线程局部变量中 存储动态分配的数据
在独立线程中运行 的回调函数
为线程设定首选的 处理器
额外的多线程技术 资源
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
可以在
4. 在 LabWindows/CVI 的辅助线程中运行代 码
单线程程序中的线程 被称为主线程。在用 户告诉操作系统开始 执行特定的程序时, 操作系统将创建主线 程。在多线程程序 中,除了主线程外, 程序还通知操作系统 创建其他的线程。这
些线程被称为辅助线 程。主线程和辅助线 程的主要区别在于它 们开始执行的位置。 操作系统从main 或者WinMain 函数开始执行主线 程,而由开发人员来 指定辅助线程开始执 行的位置。
下的 LabWindows/CVI 库函数树状图中找到 所有的多线程函数。
在典型的 LabWindows/CVI 多线程程序中,开发 者使用主线程来创 建、显示和运行用户 界面,而使用辅助线 程来进行其它时间要 求严格的操作,如数 据采集等。 LabWindows/CVI
1/8
www.ni.com
在典型的 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中的 线程池函数来代替异
步定时器函数。
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变量(
由于线程1在增加变 量值并将其写回之前 被打断,所以变量 count的值被设 为6而不是7。操作 系统为系统中地每一 个线程的寄存器都保 存了副本。即使编写 了count++这 样的代码,用户还是
会遇到相同的问题, 因为处理器会将代码 按照多条指令执行。 注意,特定的时序状 态导致了这个错误。 这就意味着程序可能 正确运行1000 次,而只有一次故 障。经验告诉我们,
有着数据保护不当问 题的多线程程序在测 试的过程中通常是正 确的,但是一到客户 安装并运行它们时, 就会发生错误。
需要保护的数据类型
只有程序中的多个线 程可以访问到的数据 是需要保护的。全局 变量、静态局部变量 和动态分配内存位于 通常的内存空间中, 程序中的所有线程都 可以访问它们。多个 线程对内存空间中存
储的这些类型的数据 进行并发访问时,必 须加以保护。函数参 数和非静态局部变量 位于堆栈上。操作系 统为每个线程分配独 立的堆栈。因此,每 个线程都拥有参数和 非静态局部变量的独
立副本,所以它们不 需要为并发访问进行 保护。下面的代码显 示了必须为并发访问 而保护的数据类型。
= 6, register = 6)。
count
int globalArray [1000]; // Must be protected
static staticGlobalArray [500];// Must be protected
int globalInt; // Must be protected
2/8
www.ni.com
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)作 为标识来为线程安全 变量创建自定义的访 问函数名称。
注意事项:
在第一次访问线程安 全变量前首先调用一 次 InitializeVarName UninitializeVarName GetPointerToVarName ReleasePointerToVarName
GetPointerToVarName ReleasePointerToVarName GetPointerToVarName ReleasePointerToVarName
ReleasePointerToVarName run-time error
如果需要对变量值进 行设定而不需要考虑 其当前值,那么请调 用 SetVarName GetVarName GetVarName
下面的代码显示了如 何使用线程安全变量 作为前面例子中提到 的计数变量。
不匹配调用”一节中 进行进一步讨论。
maxGetPointerNestingLevel
前面没有与之相匹配 的
参数将在“检测
GetPointerToVarName
GetPointerToVarName
DefineThreadSafeScalarVar (int, Count, 0);
3/8
www.ni.com
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
GetPointerToVarName
那么可将
maxGetPointerNestingLevel
参数设为一个大于零 的整数。例如,下面 的代码将
maxGetPointerNestingLevel
参数设定为1,因此 它允许对
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 */
4/8
www.ni.com
... /* 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.
下面的 Windows SDK函数可以获取 线程锁对象但在返回 时并不释放这些对 象。注意,这不是完 整的列表。
This is not a comprehensive list.
Note:
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;
5/8
www.ni.com
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;
}
注意事项:如果使用
因此,作为优化,编 译器可能只使用 quit变量在 while循环条件 中的初始值。使用 volatile关 键字是告知编译器另 一个线程可能会改变 quit变量的值。 这样,编译器在每次 循环运行时都使用更
新过后的quit变 量值。
有些时候,当主线程 进行其他任务的时候 需要暂停辅助线程的 运行。如果你暂停正 在运行操作系统代码 的线程,可能会使得 操作系统处于非法状 态。因此,在需要暂 停的线程中需要始终 调用
Windows SDK的
展示了如何使用它 们。
关键字,这段代码在 经过优化的编译器 (如 Microsoft Visual C++)后功能是正 常的。优化的编译器 确定while循环 中的代码不会修改 quit变量的值。
function函 数。这样,可以确保 线程在运行关键代码 时不被暂停。在另一 个线程中调用 Windows SDK的
function是 安全的。下面的代码
SuspendThread
ResumeThread
volatile
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线程 池(调用
9. 消息处理
每个创建了窗口的线 程必须对 Windows消息 进行处理以避免系统 锁定。用户界面库中 的
的循环。用户界面库 中的
个线程都需要调用
functions 函数在每次被调用时 对Windows消 息进行处理。如果下 列情况中的之一被满 足,那么程序中的每
)时,可以设定池中 各线程的默认优先 级。
regularly 函数来处理 Windows消 息。
GetUserEvent ProcessSystemEvents
ProcessSystemEvents
CmtNewThreadPool
GetUserEvent
or和
RunUserInterface
function函 数包含了处理 LabWindows/CVI 用户界面事件和 Windows消息
线程创建了窗口但没 有调用 RunUserInterface
线程创建了窗口并调 用了 RunUserInterface RunUserInterface ( )
但是,在代码中的某 些地方不适合用于处 理Windows消 息。在 LabWindows/CVI 的用户界面线程中调 用了 GetUserEvent ProcessSystemEvents RunUserInterface
Utility Library中的 多线程函数会造成线 程在循环中等待,允 许你指定是否在等待 线程中对消息进行处 理。例如, CmtWaitForThreadPoolFunctionCompletion Option Windows
有的时候,线程对窗 口的创建不是那么显 而易见的。用户界面 库函数如
Windows API函数创建了隐 藏的背景窗口。为了 避免系统的锁定,必 须在线程中对使用这 两种方法创建的窗口 的Windows消 息进行处理。
10. 使用线程 局部变量
线程局部变量与全局 变量相似,可以在任 意线程中对它们进行 访问。但是,全局变 量对于所有线程只保 存一个值,而线程局 部变量为每个访问的 线程保存一个独立的 值。当程序中需要同
时在多个上下文中进 行相同的任务,而其 中每个上下文都对应 一个独立的线程时, 通常需要使用到线程 局部变量。例如,你 编写了一个并行的测 试程序,其中的每个 线程处理一个待测单
元,那么你可能需要 使用线程局部变量来 保存每个单元的特定 信息(例如序列 号)。
Windows API提供了用于创 建和访问线程局部变 量的机制,但是该机 制对每个进程中可用 的线程局部变量的数 目进行了限定。 LabWindows/CVI Utility Library中的 线程局部变量函数没
有这种限制。下面的 代码展示了如何创建 和访问一个保存了整 数的线程局部变量。
LoadPanel CreatePanel FileSelectPopup
用户界面库函数外, 各种其它的 LabWindows/CVI 库函数和
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;
. . .
6/8
www.ni.com
. . .
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);
7/8
www.ni.com
// 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);
}
一些分配的资源必须 在分配到它们的线程 中释放。这些资源被 称为拥有线程关联 度。例如,面板必须 在创建它的线程中销 毁掉。在调用
程中调用被称为
函数,这个参数指定 了调用销毁回调函数 的线程的ID号。你 可以使用这个线程 ID来确定是否可以 直接释放掉拥有线程 关联度的资源还是必 须在正确的线程中调 用Toolslib 中的
PostDeferredCallToThreadAndWait
线程局部变量销毁回 调函数。 Utility Library为每 一个访问过该变量的 线程调用一次销毁回 调函数。它将
CmtDiscardThreadLocalVar
CmtDiscardThreadLocalVar
threadID
Utility Library在线
参数传递给销毁回调
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库中,可 以调用
GPIB/GPIB 488.2库会创建 用于执行回调函数的 线程。
在 LabWindows/CVI 的虚拟仪器软件构 架 (VISA) 库中,你可以调用
程,或者对每个
VISA库调用已注 册的事件句柄。
在 LabWindows/CVI VXI库中,每个中 断或回调函数类型都 有自己的回调注册和 使能函数。例如,为 了接收NI-VXI 中断,你必须调用
建的独立线程来执行 回调函数。对于同一 进程中所有的回调函 数,VXI都使用相 同的线程。
13. 为线程设 定首选的处理器
可以使用平台SDK 中的
LabWindows/CVI Utility Library中的 CmtGetNumberOfProcessors
可以使用平台SDK 中的
平台SDK中的
这些函数只有程序在 装有 Microsoft Windows XP/2000/NT 4.0系统的多处理 器计算机上运行才有 效果。 Microsoft Windows 9x系列的操作系统 不支持多处理器计算 机。
14. 额外的多 线程技术资源
SetThreadAffinityMask SetThreadAffinityMask mask SetProcessAffinityMask mask
viInstallHandler ViSession VISA (I/O ) VISA
GPIB/GPIB 488.2库调用的 回调函数。你可以为 每一个电路板或器件 指定一个回调函数。 可以为事件指定调用 的回调函数。
CmtGetThreadPoolFunctionAttribute ATTR_TP_FUNCTION_THREAD_HANDLE
VISA可能会对一 个进程中的所有回调 函数使用同一个线
LabWindows/CVI Utility Library中的
ViSession
使用单独的线程。 你需要为某个指定的 事件类型调用
viEnableEvent
SetThreadIdealProcessor
SetProcessAffinityMask
SetVXIintHandler EnableVXIint
VXI库使用自己创
ibnotify
()
使用 LabWindows/CVI 网页广播来学习使用 ANSI C语言以 发挥多核处理的能力
了解更多在 Windows和实 时操作系统上使用 LabWindows/CVI 进行对称式处理的知 识
学习如何使用 LabWindows/CVI 来调试多核 ANSI C应用程 序
学习更多关于 LabWindows/CVI 的知识
多核编程基础系列白 皮书
8/8
www.ni.com