多线程技术

多线程技术

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

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

目录

  1. 进行多线程编程的原因
  2. 选择合适的操作系统
  3. LabWindows/CVI中的多线程技术简介
  4. 在LabWindows/CVI的辅助线程中运行代码
  5. 保护数据
  6.  避免死锁
  7.  监视和控制辅助线程
  8.  进程和线程优先级
  9.  消息处理
  10.  使用线程局部变量
  11.  在线程局部变量中存储动态分配的数据
  12.  在独立线程中运行的回调函数
  13.  为线程设定首选的处理器
  14.  额外的多线程技术资源

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 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变量(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, VarNamemaxGetPointerNestingLevel)宏创建以下访问函数:

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

至页首

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变量的子集。

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

				
				

多线程技术原理

04-15 484

首先是其继承关系如下:通过观察上面四种线程池的源码:如:newFixedThreadPool[java] view plain copypublic static ExecutorService ne... 来自: Ned_mahone的博客

		




		

男人行房时间短?老中医说:多吃它。时间延长40分钟 万源鑫裕 · 燨燚
		

    现在只要出去面试,关于“Java多线程”的问题,几乎没有一家单位不问的,可见其重要性。于是博主抽空研究了一下,确实很有意思!以下是我综合整理了网上的各种资料,和个人的一些…

来自: qq296398300的博客



		

 
数字接口设备是实现标准422串行总线到自定义串行总线转换的专用通信设备。数字接口测试系统根据数字接口设备的工作原理,输出422串行数据和自定义串行总线的控制信…

来自: aristolto的专栏



		

进程:进程是操作系统结构的基础,是一次程序的执行,是一个程序及其数据在处理机上顺序执行时所发生的活动,是程序在一个程序集合上运行的过程,它是系统进行资源分配和调度的一个基本单位。
线程:线程是进…

来自: supersnow0622的专栏



		

多线程基础
 …

来自: 李达



		

各类自动化工装夹具的研发与制造 比优特 · 顶新
		

#include
#include
#include
#include
#include
#include
#include
#include
#include


来自: xiaoyuxianshenging的博客



		


		

#include
#include
int CVICALLBACK ThreadFunc…

来自: 思维逆逝



      
夨落旳尐孩
夨落旳尐孩

关注 319篇文章

种瓜大爷
种瓜大爷

关注 314篇文章

温柔狠角色
温柔狠角色

关注 272篇文章

Enrica_Shi
Enrica_Shi

关注 69篇文章

进程:每个应用程序对应一个进程;
线程:一个进程至少包含一个线程,线程要和CPU进行交互。
 
多线程并发(可以理解为):"同一时间"执行多件事情,如程序一边执行,一边计数(计…

来自: abcd5711664321的博客



		

多线程这个令人生畏的“洪水猛兽”,很多人谈起多线程都心存畏惧。在Android开发过程中,多线程真的很难吗?多线程程序的“麻烦”源于它很抽象、…

来自: 张勤一



本地一位炒股大爷酒后吐真言:炒股20年坚持只看一指标 陕西信息科技 · 燨燚
		




		




		

传统线程技术的回顾

  1. 多线程的创建(两种)

通过创建线程对象,复写run(),通过start()调用该线程。

本质是继承

Thread thread =…

来自: weixin_41263632的博客



		




		

克拉玛依股王凭20年资深股经探得1定律分析法,赢数千万身家! 意欧迅网络 · 燨燚
		




		

多图下载.jpg

首先,它是一个tableView,联网以后获取cell上面的…

来自: Enrica_Shi的博客



			




		

更好的资源利用率
在某些情况下能简化程序设计
高响应程序
更好的资源利用率假设一个应用程序在本地文件系统中…



		

public class Demo2 {

public static void main(String[] args) {
SaleTicket...
					
                    						来自:	 mischen520的博客
                    					

不停抽插,就是不想射有多爽! 老中医说:多吃他就可以做到! 通聚商贸 · 燨燚
		

package com.itheima.download;

import java.io…

来自: 想想挺简单的博客



		




		




		




		

克拉玛依竟有一位51岁股市奇才,炒股2年靠1个铁律,存款惊哭老婆 潮望 · 燨燚
		

其实C++语言本身并没有提供多线程机制(当然目前C++ …

来自: csx66406602的博客



		

程序功能:求从1一直到 APPLE_MAX_VALUE (100000000) 相加累计的和,并赋值给 apple 的a 和b ;求 orange 数据结构中的 a[i]+b[i ] …

来自: sharemyfree的专栏



		

ImportNew
首页所有文章资讯Web架构基础技术书籍教程Java小组工具资源
Java线程面试题 Top 50
2…

来自: u011163372的博客



		




		

老股民酒后无意说漏:20年炒股 坚持只看1指标 昂可电子 · 燨燚
		

创建两个线程,创建时即进入阻塞状态,然后根据主线程的input值,去唤醒或阻塞线程执行。
//简单的多线程创建,执行,挂起,终止的例子
//利用win32 A…

来自: timeshark的专栏



		




		

最近在制作一个Galgame的骨架。其实Galgame是技术含量最…

来自: 开发游戏,辉煌全中国



		

什么是线程:进程中独立运行的子任务。进程让操作系统的并发性成为可能,而线程让进程的内部并发成为…

来自: 时间慢慢的博客



		

紧急通知:吸这两种烟的人,快去医院检查!. 华佰科技 · 燨燚
		

    在前面的一篇文章里我对java threadpool的几种基本应用方法做了个总结。Java的线程池针对不同应用的场景,主要有固定长度类型、可变长度类型以及定时执行等几种。针对这几种…

来自: jubaoquan的博客



		




		




		




			

		




		




		

local对象。
基本思路如下:
1)通过__thread而不是pthread_getspecific来声明thread


来自: vinowan的专栏



		




		

		

请考虑这样一个饭店,它有一个厨师和一个服务员。这个服务员必须等待厨师准备好膳食。当厨师准备好时,他会通知服务员,之后服务员上菜,然后返回继续等…

来自: HeTingwei的博客



		

public class  T2{
public st…

来自: tpstu的专栏



		




		

一、前言

        Sql是最重要的关系数据库操作语言,现在基本上任何与数据库相关的操作都离不开sql。所以说sql功能是很强大的。

        但是Sql对表的操作…

来自: 愤怒的懒洋洋的博客



		

一、前言

        上一章节我们说的是swagger-ui也就是swagger1,接下来我们说的是swagger升级版swagger2

    …

来自: 愤怒的懒洋洋的博客



		




		

一、前言

        MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置…

来自: 愤怒的懒洋洋的博客



		

1.用户的添加

useradd iceFrog

passwd iceFrog

更改密码:echo “dota”|passwd -stdin iceFrog

切换用户:su …

来自: 唐三十胖子的博客



		

方法一:(更新时间:2018/4/8)v3.3

注册时,在打开的Lice…

来自: 唐大帅的编程之路



		

因公司的需求,需要做一个爬取最近上映的电影、列车号、航班号、机场、车站等信息,所以需要我做一个爬虫…

来自: 昌昌



		




		




		

现在越来越流行在线看视频了,但是对于我得收藏癖爱好者,还是希望可以有比较好的资源网站的,尤其是种子、磁力链网站。所以就整理了一份干净、好用的TOP10出来:

先推荐一个下载磁力链的工具:

马…

来自: YXAPP的技术分享



		

转载请标明出处: http://blog.csdn.net/forezp/article/details/70148833
本文出自方志朋的博客

错过了这一篇,你可能再也学不会 Sp…

来自: 方志朋的专栏



		




		

引入element后,由于网络速度较慢,发现页面运行起来,比较卡顿,于是将其下载到本地
1.下载静态资源
element-ui 官网 :http://element…



		

vue-cli3脚手架/单文件/环境搭建。vue3.x版本相对于2.x版本做了许多地方的优化,个人总结主要还是使用上使开发者更加一目了然,配置起来也更加的方便,减少了各种webpack里面的lo…

来自: 欢迎来到☆槿畔☆的博客



		




		




		

前言
MVVM是一种架构模式,本文会涉及一小点vue代码,以及一篇简单的springboot的代码,建议在阅读本文档前先对这两门技术做一些学习。
什么是MVVM
MVVM是三个单词…



		

主要功能:
1、 解决服务之间代码耦合
2、 使用消息队列,增加系统并发处理量
主要应用场景:
1…

来自: 熊局长的博客



		

一:数据库缓存
二:静态缓存
三:动态缓存
1.数据库缓…

来自: 猪精的博客



		

下载地址https://s3.amazonaws.com/psiphon/web/mjr4-p23r-puwl/zh/download.html#direct

 …

来自: www.mayixiaocao.cn



		

在学习Vue的过程中,官方网站都是给了非常详细的介绍,所以初始化大型单页应用,官网给的参考资料地址:https://cn.vuejs.org/v2/guide/installation…



		




		

一、前言

Sql是最重要的关系数据库操作语言,现在基本上任何与数据库相关的操作都离不开sql。所以说sql功能是很强大的。

我们常用的sql关键字不外乎 group by…

来自: 愤怒的懒洋洋的博客



		

最新版的Proxyee-down为3.12(2018.10更新),因为作者在3.x后的版本中并未发布exe版…

来自: shadandeajian的博客



		

1
【单选题】我国陆地领土面积排名世界第几?(C)
A、1
B、2
C、3
D、4
2
【单选题】以下哪个国家不属于金砖五国(BRICS)?(B)
A、中国
B、日本
C…

来自: ling_wang的博客



		

 

 

 

本文由@唐三十胖子出品,转载请注明出处。  文章链接:https://blog.csdn.net/iceSony/article/details/84667551

 

 

 …

来自: 唐三十胖子的博客



		

问题总结:
在Windows平台下如果PHP使用的是IIS的话那么php在上传文件时是先将文件上传到一个临时目录下的


来自: psw的博客



		

大概想了一个方法,虽然看起来也不怎么专业,但感觉…

来自: u010569419的博客



		

思路就是1.获得点击左键时当前鼠标的坐标 2.获得移动后鼠标的坐标 3.窗体的坐标=移动后的鼠标坐标-移动前的鼠标坐标

pr…

来自: Maybe_ch的博客



		




		




		

https://www.centos.org/download/

根据下图所示,查询以前版本,为什么不用最新版?你可以想想!

       滑动到第二张图所示区域,我…

来自: u010569419的博客



		
    

你可能感兴趣的:(多线程技术)