本文总结Windows API编程之多线程,以供大家参考。
作者:tyc611,2007-05-13
在Windows的多线程编程中,创建线程的函数主要有CreateThread和_beginthread(及_beginthreadex)。
CreateThread 和 ExitThread
使用API函数CreateThread创建线程时,其中的线程函数原型:
DWORD WINAPI ThreadProc(LPVOID lpParameter);
在线程函数返回后,其返回值用作调用ExitThread函数的参数(由系统隐式调用)。可以使用GetExitCodeThread函数获得该线程函数的返回值。
当线程函数的起始地址无效(或者不可访问)时,CreateThread函数仍可能成功返回。如果该起始地址无效,则当线程运行时,异常将发生,线程终止。并返回一个错误代码。
使用CreateThread创建的线程具有THREAD_PRIORITY_NORMAL的线程优先级。可以使用GetThreadPriority和SetThreadPriority函数获取和设置线程优先级值。
系统中的线程对象一直存活到线程结束,并且所有指向它的句柄都通过调用CloseHandle关闭后。
_beginthread 和 _endthread (_beginthread & _endthread)
对于使用C运行时库里的函数的线程应该使用_beginthread和_endthread这些C运行时函数来管理线程,而不是使用CreateThread和ExitThread。否则,当调用ExitThread后,可能引发内存泄露。
在使用_beginthread或者_beginthreadex创建线程时,应该包含头文件<process.h>,并且需要设置多线程版本的运行时库。「Project Settings」--> 「C/C++」-->「Category」-->「Code Generation」-->「Use Run-Time Library」-->「Multithreaded」和「Debug Multithreaded」。这相当于给编译添加了一个编译选项/MT,使编译器在编译时在.obj文件中使用libcmt.lib文件名而不是libc.lib。连接器使用这个名字与运行时库函数连接。
可以调用_endthread和_endthreadex显示式结束一个线程。然而,当线程函数返回时,_endthread和_endthreadex被自动调用。endthread和_endthreadex的调用有助于确保分配给线程的资源的合理回收。_endthread自动地关闭线程句柄,然而_endthreadex却不会。因此,当使用_beginthread和_endthread时,不需要显示调用API函数CloseHandle式关闭线程句柄。该行为有别于API函数ExitThread的调用。_endthread和_endthreadex回收分配的线程资源后,调用ExitThread。
当_beginthread和_beginthreadex被调用时,操作系统自己处理线程栈的分配。如果在调用这些函数时,指定栈大小为0,则操作系统为该线程创建和主线程大小一样的栈。如果任何一个线程调用了abort、exit或者ExitProcess,则所有线程都将被终止。
这里给出一个MSDN上的例子,其演示了多线程的使用及Windows控制台编程:
|
文件: |
BEGTHRD.rar |
大小: |
1KB |
下载: |
下载 |
|
同步与互斥
在一个进程中,多个线程可以使用同一函数作为其线程函数,或者多个线程函数直接或间接调用同一个函数。此时,该函数中使用的自动(局部)变量(存储于栈上)在每个线程中都有一个独立的对象,互不干扰(虽然是同一函数,但被不同的线程所调用,可以把它们想象成不同的函数)。但所有静态变量却是共享的(包括函数内静态变量)。此时就要注意该静态对象的同步与互斥了,可以使用信号和临界区来实现。
当辅助线程正在绘制它的输出时,可能主线程刚好收到一个WM_PAINT或者WM_ERASEBKGND消息。这时,可以使用临界区来避免两个线程在同一窗口上同时绘画所带来的任何可能问题。但实验表明,Windows 98是串行地访问图形绘画函数的,也就是说,在一个线程未完成绘制前,另一个线程不是能够开始绘制的。但Windows 98文档对一个地方未串行化给予了警告,那就是在实用GDI对象时,例如画笔、画刷、字体、位图、区域和调色板。因此,这里就需要使用临界区来避免可能的问题。当然,更好的方式是不在线程间共享GDI对象。
对于多线程编程,最好的方式是主线程负责创建程序的所有窗口,包括这些函数的窗口过程,并处理传给这些窗口的消息;而辅助线程执行后台作业或者冗长的作业。
通常,我们使用WM_TIMER消息实现动画。但是如果辅助线程不创建一个窗口,它就无法接收消息。而如果没有延时,动画总是运行得太快。这时,我们可以使用Sleep函数。线程调用Sleep函数来挂起它自己。传给Sleep的参数为0时,致使线程放弃它剩余的时间片。当一个线程调用Sleep挂起它自己时,程序中其它线程仍继续运行,不受影响。通常,不应该在主线程中使用Sleep函数,因为它会影响消息处理速度。
临界区
临界区实现的是互斥技术。在任何时刻,只有一个线程能进入临界区。只有在该线程离开临界区后,其它线程才能进入。临界区对象不能被移动或者拷贝,进程也不能够修改该对象,而必须把它看作逻辑上不透明的(小黑箱)。使用由Win32 API提供的临界区函数来管理临界区对象。
要使用临界区,首先需要定义一个临界区对象,此时,进程负责分配相应内存:
CRITICAL_SECTION cs;
然后,临界区对象必须由程序中某个线程初始化:
InitializeCriticalSection(&cs);
当初始化完该临界区对象后,线程可以调用下面的函数进入临界区:
EnterCriticalSection (&cs);
进入临界区后,该线程被认为拥有该入临界区对象。因此,其它线程调用EnterCriticalSection想进入该入临界区时,会被挂起。赶到拥有临界区对象的线程离开临界区后,它才有机会获得该临界区对象。一旦一个线程拥有临界区,那么它可以再调用EnterCriticalSection或者TryEnterCriticalSection而不会阻塞它自己的执行。这防止了一个线程因等待它自己所拥有的临界区而出现死锁。(TryEnterCriticalSection与TryEnterCriticalSection的区别:前者无论是否获得临界区,都立即返回;而后者在获得临界区前一直处理阻塞状态。TryEnterCriticalSection可以通过函数的返回值判断是否获得临界区,如果没有,可以做其它事情,而不是处于等待状态。)
离开临界区:
LeaveCriticalSection(&cs);
当临界区不再需要时,可将其删除,从而释放相应系统资源:
DeleteCriticalSection (&cs);
使用临界区有一个限制,它只能用于同一进程内的线程间的互斥。而在某些情况下,需要协调两个不同进程对同一资源的共享(如共享内存)。此时,已不能再使用临界区做到,需要借助于互斥对象(mutex object)来实现(注:mutex是一个合成词[mutual exclusion])。
事件信号
当需要线程间的同步时,可以使用信号机制,这里介绍Windows编程中的事件信号。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
// pointer to security attributes
BOOL bManualReset, // flag for manual-reset event
BOOL bInitialState, // flag for initial state
LPCTSTR lpName // pointer to event-object name
);
创建一个有名或无名事件对象。第一个参数lpEventAttributes:如果该事件不被子进程所继承,则通常置为NULL。第二个参数bMenualReset:如果为TRUE,你必须手动调用ResetEvent函数设置事件状态为nonsignaled状态;如果为FALSE,则系统会在一个等待线程就绪后(即一个WaitForSingleObject返回后)自动重置为nonsignaled状态。第三个参数bInitialState:如果为TRUE,则初始状态为signaled;否则为nonsignaled。第四个参数lpName:指向事件对象的名字(如果为NULL,则为无名事件对象)。如果不为NULL,则首先搜索现有的对象是否与该名字一致,再进一步处理。如果该对象不需要共享的话,一般置为NULL。
BOOL SetEvent(HANDLE hEvent); // 设置hEvent事件状态为signaled。
BOOL ResetEvent(HANDLE hEvent); // 设置hEvent事件状态为nonsignaled。
等待函数。Win32 API提供了一组等待函数来允许线程阻塞它自己的执行。分为三类等待函数:1)单对象;2)多对象;3)alertable。等待函数不会返回,除非指定的条件满足。等待函数的类型决定了要检验的条件。当一个等待函数被调用时,它首先检查条件是否满足。如果不满足,则该线程进入等待状态,进入相应事件的等待队列,直到相应信号将其唤醒。这里重点介绍WaitForSingleObject函数。
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
当等待的对象为signaled状态或者超时时,该函数返回。该函数不仅用于Event对象,还可用于其它对象,
如Job、Mutex、Semphore、Thread等等。另外,对于创建了窗口的线程,不要用这个函数,否则容易发生死锁,此时应考虑使用MsgWaitForMultipleObjects 或者MsgWaitForMultipleObjectsEx。
最后,使用CloseHandle函数来关闭句柄。当进程结束时,系统自动关闭句柄。当被关闭的句柄是该事件对象的最后一个句柄时,该对象被销毁。
线程局部存储(TLS)
一个多线程程序中,全局变量(及分配的内存)被所有线程所共享。函数的静态局部变量也被所有使用该函数的线程所共享。一个函数中的自动变量对每一个线程是唯一的,因为它们存储于堆栈上,而每个线程都有
他们自己的堆栈。有时,我们需要对每一个线程唯一的持续性存储。例如,C函数strtok就需要这种存储。不幸的是,C语言不支持这种变量。但是Windows提供了四个API函数来实现这种机制。我们把这种存储称为线程局部存储(TLS,Thread Local Storage)。
首先,定义一个结构,把对每个线程唯一的数据包含在该结构中。例如:
typedef struct {
int one;
int two;
} DATA, *PDATA;
然后,主线程调用TlsAlloc函数来为进程获得一个TLS索引:tlsIndex = TlsAlloc();
该TLS索引可以存储于一个全局变量或者通过线程函数的参数传递给其它线程。
每个需要使用该TLS索引的线程,先动态分配内存,然后调用TlsSetValue函数将该内存关联到该TLS索引(及该线程): TlsSetValue(tlsIndex, GlobalAlloc(GPTR, sizeof(DATA));
此时,线程直接或间接调用的函数可以通过如下方式获得该线程的TLS存储区域:
PDATA pdata;
pdata = (PDATA) TlsGetValue(tlsIndex);
此时,就可以使用该线程的TLS存储区的变量了。
当线程函数终止时,它应该释放它所分配的动态空间: GlobalFree(TlsGetValue(tlsIndex));
当所有使用TLS的线程都终止后,主线程应当释放该TLS存储空间: TlsFree(tlsIndex);
常量TLS_MINIMUM_AVAILABLE定义了每个进程可用的TLS索引的最小值(>=64)。
TLS可以以一种更简单的方式使用,那就是通过Winodws对C所作的扩展关键字__declspec和扩展存储类型修饰符thread。例如:
__declspec(thread) int global_tls_i = 1; // 在函数外部,声明一个TLS变量
__declspec(thread) static int local_tls_i = 2; // 在函数内部声明一个静态TLS变量
最后,附带一个MSDN里关于TLS使用的例子:
|
文件: |
TLS_Test.rar |
大小: |
0KB |
下载: |
下载 |
|