Windows多线程程序设计
一. 结束线程:
可以利用GetExitCodeThread函数,该函数会传回线程函数的返回值,然而该函数的一个糟糕行为是:当线程还在进行,尚未有所谓结束代码时,它会传回TRUE表示成功,如果这样第二个形参lpExitCode指向的内存区域中应该放的是STILL_ACTIVE,要注意这种行为,也就是说你不可能从其返回值中知道“到底线程还在运行还是它已结束”,而应根据lpExitCode中是否为STILL_ACTIVE来判断。
For(;;)
{bool rc; rc = GetExitCodeThread(HANDLE,lpExitCode);
If(rc&&(*lpExitCode)!= STILL_ACTIVE)
//线程结束}
强制结束一个线程可以利用函数void ExitThread(DWORD dwExitCode);形参指定此线程之结束代码,此函数类似于c runtime library中的exit()函数,因为它可以在任何时候被调用并且绝不会返回,任何代码若放在此行之下,保证不会被执行。
程序启动后就执行的那个线程称为主线程,主线程有两个特点,第一,它必须负责GUI程序中的主消息循环;第二,这一线程的结束(不论是因为返回或因为调用了ExitThread)会使得程序中的所有线程都被强迫结束,程序也因此而结束,其他线程没有机会做清理工作。所以在main或winmain结束之前,应先等待所有的线程都结束。
诊断宏:
#pragma comment( lib, "USER32" )
#include <crtdbg.h>
#define MTASSERT(a) _ASSERTE(a)
#define MTVERIFY(a) if (!(a)) PrintError(#a,__FILE__,__LINE__,GetLastError())
__inline void PrintError(LPSTR linedesc, LPSTR filename, int lineno, DWORD errnum)
{
LPSTR lpBuffer;
char errbuf[256];
#ifdef _WINDOWS
char modulename[MAX_PATH];
#else // _WINDOWS
DWORD numread;
#endif // _WINDOWS
FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER
| FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
errnum,
LANG_NEUTRAL,
(LPTSTR)&lpBuffer,
0,
NULL );
wsprintf(errbuf, "\nThe following call failed at line %d in %s:\n\n"
" %s\n\nReason: %s\n", lineno, filename, linedesc, lpBuffer);
#ifndef _WINDOWS
WriteFile(GetStdHandle(STD_ERROR_HANDLE), errbuf, strlen(errbuf), &numread, FALSE );
Sleep(3000);
#else
GetModuleFileName(NULL, modulename, MAX_PATH);
MessageBox(NULL, errbuf, modulename, MB_ICONWARNING|MB_OK|MB_TASKMODAL|MB_SETFOREGROUND);
#endif
exit(EXIT_FAILURE);
}
多线程程序设计成功的关键:
(1) 各线程的数据要分离开来,避免使用全局变量。
(2) 不要在线程之间共享GDI对象
(3) 确定你知道你的线程状态,不要径自结束程序而不等待它们的结束。
(4) 让主线程处理用户界面。
二.关于Wait…()函数
DWORD WaitForSingleObject(
HANDLE hHandle;
DWORD dwMilliseconds);
参数:
hHandle---等待对象的handle(代表一个核心对象)
dwMilliseconds—等待的最长时间,时间终了,即使handle尚未称为激发状态,此函数还是要返回,此值可以是0(代表立刻返回),也可以是INFINITE代表无穷等待。
返回值:
如果函数失败,则传回WAIT_FAILED,这时候你可调用GetLastError取得更多信息,此函数的成功有三个因素:
1. 等待的目标(核心对象)变成激发状态,这种情况下返回值将为WAIT_OBJECT_0.
2. 核心对象变成激发状态之前,等待时间终了,这种情况下返回WAIT_TIMEOUT.
3. 如果一个拥有mutex(互斥器)的线程结束前没有释放mutex,则传回WAIT_ABANDONED.
获得一个线程对象的handle之后,WaitForSingleObject要求操作系统让线程1睡觉,直到以下任何一种情况发生:
1. 线程2结束
2. dwMilliseconds时间终了,该值系从函数调用后开始计算。
由于操作系统追踪线程2,所以即使线程2失事或被强迫终止,该函数也能正常工作。
关于该函数的第二个参数,若设定为0,可使你能够检查handle的状态并立刻返回,没有片刻停留,如果handle已经备妥,那么这个函数会成功并传回WAIT_OBJECT_0,否则,这个函数立刻返回并传回WAIT_TIMEOUT.
可被WaitForSingleObject使用的核心对象有两种状态:激发与未激发。Wait函数会在目标变成激发状态时返回。
当线程正在执行时,线程对象处于未激发状态,当线程结束,线程对象就被激发了,因此,任何线程如果等待的是一个线程对象,将会在等待对象结束时被调用,因为当时线程对象自动变成激发状态。
Win32的核心对象激发状态的意义
对象 | 说明 |
Thread | 当线程结束时,线程对象即被激发,当线程还在进行时,则对象处于未激发状态,线程对象由CreateThread或CreateRemoteThread产生 |
Process | 当进程结束时,进程对象即被激发,当进程还在进行时,则对象处于未激发状态,CreateProcess或OpenProcess会传回一个进程对象的handle |
Change Notification | 当一个特定的磁盘子目录中发生一件特别的变化时,此对象即被激发,此对象系由FindFirstChangeNotification产生 |
Console Input | 当console窗口的输入缓冲区中有数据可用时,此对象处于激发状态,CreateFile或GetStdFile两函数可以获得console handle. |
Event | Event对象的状态直接受控于应用程序所使用的三个Win32函数:SetEvent(), PulseEvent,RestEvent。CreateEvent或OpenEvent都可以传回一个handle,Event对象的状态可被操作系统设定---如果使用于overlapped操作时。 |
Mutex | 如果mutex没有被任何线程所拥有,它就是处于激发状态,一旦一个等待mutex的函数返回了,mutex也就自动重置为未激发状态,CreateMutex或OpenMutex都可以获得一个mutex handle。 |
Semaphore | Semaphore有点像mutex,但它有个计数器,可以约束其拥有者(线程)的个数,当计数器大于0时,Semaphore处于激发状态,当计数器等于0时,semaphore处于未激发状态,CreateSemaphore或OpenSemaphore可以传回一个semaphore handle。 |
WaitForMultipleObject函数:
DWORD WaitForMultipleObject(
DWORD nCount,
CONST HANDLE *lpHandles,
BOOL bWaitAll,
DWORD dwMillisencods)
参数:
nCount—表示lpHandles所指之handles数组的元素个数,最大容量为MAXIMUM_WAIT_OBJECTS.
lpHandles—指向一个由对象handles所组成的数组,这些handles不需要为相同的类型。
bWaitAll—如果此为true,表示所有的handles都必须激发,此函数才得以返回,否则此函数将在任何一个handle激发时返回。
dwMilliseconds—当该事件长度终了时,即使没有任何handles激发,此函数也会返回,此值可为0,以便测试,亦可指定INFINITE,表示无穷等待。
返回值:
1. 如果因时间终了而返回,则返回值是WAIT_TIMEOUT.
2. 如果bWaitAll是TRUE,那么返回值将是WAIT_OBJECT_0.
3. 如果bWaitAll是FALSE,那么返回值减去WAIT_OBJECT_0,就表示数组中的哪一个handle被激发了。
4. 如果你等待的对象中有任何mutexes,那么返回值可能从WAIT_ABANDONED_0到WAIT_ABANDONED_0+nCount-1.
5. 如果函数失败,它会传回WAIT_FAILED,这时候你可以用GetLastError找出失败的原因。
GetMessage函数等待消息而不是核心对象,一旦你调用GetMessage,除非有一个消息真正进入你的消息队列,否则它不会返回,在此期间,Windows就可以自由地将CPU时间给与其他程序。
如果你正使用WaitSingleObject或WaitForMultipleObjects等待某个对象被激发,你根本没有办法回到主消息循环中去。为解决这个问题,主消息循环必须修改,使它得以同时等待消息或核心对象被激发,必须使用一个MsgWaitForMultipleObjects函数,这个函数非常类似WaitForMultipleObjects,但它会在“对象被激发”或“消息到达队列”时被唤醒而返回。
DWORD MsgWaitForMultipleObjects(
DWORD nCount,
LPHANDLE lpHandles,
BOOL fWaitAll,
DWORD dwMilliseconds,
DWORD dwWakeMask);
参数:
dwWakeMask—欲观察的用户输入消息,可以是: QS_ALLINPUT,QS_HOTKEY,QS_INPUT,QS_KEY,QS_MOUSE, QS_MOUSEBUTTON,QS_MOUSEMOVE,QS_PAINT,QS_POSTMESSAGE,QS_SENDMESSAGE,QS_TIMER.
返回值:
和WaitForMultipleObjects相比较,MsgWaitForMultipleObjects有一些额外的返回值意义,为了表示“消息到达队列”,返回值将是WAIT_OBJECT_0+nCount。
while(!quit || gNumPrinting > 0)
{
DWORD dwWake;
dwWake = MsgWaitForMultipleObjects(gNumPrintings,gPrintJobs,FALSE,INFINITE,QS_ALLEVENTS);
if(dwWake >= WAIT_OBJECT_0 && dwWake < WAIT_OBJECT_OBJECT_0+gNumPrinting)
{
//处理有信号的核心对象
}
else if(dwWake == WAIT_OBJECT_0 + gNumPrinting)
{
While(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
If(hDlgMain == NULL || !IsDialogMessage(hDlgMain,&msg))
{
if(msg.message == WM_QUIT)
{
quit = TRUE;
exitcode = msg.wParam;
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}//end while
}
}// end while
有数种情况是这个循环必须处理而却可能在它第一次设计时容易被忽略的:
1. 在你收到WM_QUIT之后,Windows仍然会传送消息给你,如果你要在收到WM_QUIT之后等待所有线程结束,你必须继续处理你的消息,否则窗口会变得反应迟钝,而且没有重绘能力。
2. 该函数不允许handles数组中有缝隙产生。所以当某个handle被激发了时,你应该在下一次调用该函数之前先把handles数组做个整理,紧压,不要只是把数组中的handle设为NULL。
3. 如果有另一个线程更改了对象数组,而那是你正在等待的,那么你需要一种新方法,可以强迫MsgWaitForMultipleObjects返回,并重新开始,以包含这个新的handle。
三. 同步控制
当线程1调用线程2时,线程1停下不动,直到线程2完成返回到线程1来,线程1才继续下去,这就是所谓的同步(synchronous),如果线程1调用线程2后,径自继续自己的下一个动作,那么两者之间就是所谓的异步(asynchronous),例如SendMessage就是同步行为,而PostMessage属于异步行为。
1. 临界区:critical section并不是核心对象,因此,没有所谓的handle这样的东西,它和核心对象不同,它存在于进程的内存空间中,你不需要使用像“create”这样的api函数来获得一个critical section handle,你应该做的是将一个类型为CRITICAL_SECTION的局部变量初始化,方法是调用InitializeCriticalSection;
VOID IniitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection),当利用毕cirtical section时,你必须调用DeleteCriticalSection清除它,但这个函数并没有“释放对象”的意义在里头。一旦critical section被初始化,每一个线程就可以进入其中—只要它通过了EnterCriticalSection这一关。当线程准备好离开cirtical section时,它必须调用LeaveCriticalSection。一旦一个线程进入一个critical section,它就能够一再地重复进入该critical section,但是每一个“进入”操作都必须有一个对应的“离开”操作。
千万不要在critical section之中调用Sleep或任何的Wait..函数。由于critical section不是核心对象,如果进入critical section的那个线程结束了或当掉了,而没有调用LeaveCriticalSection的话,系统没有办法将critical section清除,如果你需要那样的机能,你应该使用mutex。在Windows NT之中,如果一个线程进入某个critical section而在未离开的情况下就结束,该critical section会被永远锁住,而在Windows95中,如果发生同样的情况,其他等着要进入该cirtical section的线程,将获准进入,这基本上是一个严重的问题,因为你竟然可以在你的程序处于不稳定状态时进入该critical section。
2. 互斥体(mutex):mutex和critical section做相同的事情,但是它们的运作还是有差别的。(1)锁住一个未被拥有的mutex,比锁住一个未被拥有的critical section,需要花费几乎100倍的时间,因为critical section不需要进入操作系统的内核,直接在ring3级就可以进行操作。(2)Mutexes可以跨进程使用,Critical section则只能在同一个进程中使用。(3)等待一个mutex时,你可以指定“结束等待”的时间长度,但对于critical section则不行。
为了能够跨进程使用同一个mutex,你可以在产生mutex时指定其名称,这个名称对整个系统而言是全局性的,所以应保证其独一无二性,如果你指定了名称,系统中的其他任何线程就可以使用这个名称来处理该mutex,一定要使用名称,因为你没有办法把handle交给一个执行中的进程。
Mutex是一个核心对象,因此它被保持在系统核心之中,并且和其他核心对象一样,有所谓的引用计数。利用CreateMutex函数产生一个mutex。HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName)
lpMutexAttributes—安全属性,NULL表示使用默认的属性,这一指定在Win95中无效。
bInitialOwner—如果你希望“调用CreateMutex的这个线程”拥有产生出来的mutex,就将此值设为TRUE.
lpName—mutex的名称,任何进程或线程都可以根据此名称使用这一mutex,名称可以是任意字符串,只要不含\即可。
返回值:如果成功,则传回mutex的handle,否则传回NULL,调用GetLastError可以获得进一步的信息,如果指定mutex的名称已经存在,GetLastError会传回ERROR_ALREADY_EXISTS.
当不再需要一个mutex时,可调用CloseHandle将它关闭,和其他核心对象一样,mutex有一个引用计数,每次调用CloseHandle,引用计数便减1,当引用计数为0时,mutex便自动被系统销毁。
打开一个应经存在的mutex,可调用函数OpenMutex,如调用CreateMutex建立一个已经存在的mutex,会传回该mutex handle,但是GetLastError会传回ERROR_ALREADY_EXISTS.
欲获得一个mutex的拥有权,使用Win32的Wait。。。()函数,如果没有任何线程拥有那个mutex,Wait。。函数就会成功。Mutex的拥有权并非属于那个产生它的线程,而是那个最后对此Mutex进行Wait。。。操作并且尚未进行ReleaseMutex操作的线程。
在一个适当的程序中,线程绝对不应该在它即将结束前还拥有一个mutex,因为这意味着线程没有能够适当地清除其资源,为了解决这个问题,mutex有一个非常重要的特性,这性质在各种同步机制中是独一无二的。如果线程拥有一个mutex而在结束前没有调用ReleaseMutex,mutex不会被摧毁,取而代之,该mutex会被视为“未被拥有”以及“未被激发”,而下一个等待中的线程会被以WAIT_ABANDONED_0通知,不论线程是因为ExitThread而结束,或是因当掉而结束,这种情况都存在。如果其他线程正以WaitForMultipleObjects等待此mutex,该函数也会返回,传回值介于WAIT_OBJECT_0和WAIT_OBJECT_n+1之间,其中n为handle数组的元素个数,线程可以根据这个值了解到究竟哪一个mutex被放弃了,至于WaitForSingleObject则只是传回WAIT_ABANDONED_0.
CreateMutex的第二个参数允许你指定现行线程是否立刻拥有即将产生出来的mutex。是为了阻止跨进程使用时导致的race condition的发生。
3. 要在Win32环境中产生一个semaphore,必须使用CreateSemaphore函数。
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LpCTSTR lpName)
参数:
lpAttributes----安全属性,如果是NULL,就表示要使用默认属性,Win95忽略这一参数。
lInitialCount---semaphore的初值,必须大于或等于0,并且小于或等于lMaximumCount。
lMaximumCount---semaphore的最大值,这也就是在同一时间内能够锁住semaphore之线程的最多个数。
lpName---semaphore的名称,任何线程或进程都可以根据这一名称引用到这个semaphore,这个值可以是NULL,意思是产生一个没有名字的semaphore。
返回值:
如果成功就传回一个handle,否则传回NULL,不论哪一种情况GetLastError都会传回一个合理的结果,如果指定的semaphore名称已经存在,则该函数还是成功,GetLastError会传回ERROR_ALREADY_EXISTS。
如果锁定成功(利用wait函数),与mutex不同的是,你并不会收到semaphore的拥有权,因为可以有一个以上的线程同时锁定一个semaphore,所以谈semaphore的拥有权并没有太多的意义。因为没有拥有权这种概念,一个线程可以反复调用Wait函数以产生新的锁定,这和mutex不同,拥有mutex的线程不论再调用多少次wait函数,也不会被阻塞住。
BOOL ReleaseSemaphore(HANDLE hSemaphore,LONG lReleaseCount,LPLONG lpPreviousCount)
参数:
hSemaphore----Semaphore的handle
lReleaseCount—Semaphore现值的增额,该值不可以是负值或0
lpPreviousCount—借此传回semaphore原来的值。
返回值:
如果成功,则传回TRUE,否则传回FALSE,失败时可调用GetLastError获得原因。
ReleaseSemaphore对于semaphore所造成的现值的增加绝对不会超过CreateSemaphore时所指定的lMaximumCount。注意lpPreviousCount所传回来的是一个瞬间值,你不可以把lReleaseCount加上*lpPreviousCount就当做是semaphore的现值,因为其他线程可能已经改变了semaphore的值。同时与mutex不同的是,调用ReleaseSemaphore的那个线程并不一定就得是调用wait的那个线程,任何线程都可以在任何时间调用ReleaseSemaphore,解除被任何线程锁定的semaphore。CreateSemaphore的第二个参数lInitialCount,它的存在理由和CreateMutex的bInitialOwner参数的存在理由是一样的,如果你把初值设定为0,你的线程就可以在产生semaphore之后进行所有必要的初始化工作,待初始化工作完成之后,调用ReleaseSemaphore就可以把现值增加到其最大可能值。
4. EVENT.Event是核心对象,它的唯一目的就是成为激发状态或未激发状态,这两种状态全由程序来控制。为了产生一个event对象,你必须调用CreateEvent函数。
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,LPCTSTR lpName)
参数:
lpEventAttributes---安全属性,NULL表示默认属性,该属性在Win95中会被忽略。
bManualReset---如为FALSE,表示这个event将在变成激发状态(因而唤醒一个线程)之后,自动重置为非激发状态,如果是TRUE,表示不会自动重置,必须靠程序操作(调用ResetEvent)才能将激发状态的event重置为非激发状态。
bInitialState—如为TRUE,表示这个event一开始处于激发状态,如为FALSE,则表示这个event一开始处于非激发状态。
lpName---event对象的名字,任何线程或进程都可以根据这个文字名称,使用这一event对象。
返回值:
如果调用成功,会传回一个handle,如果lpName指定的event对象已经存在,传回的是该event的handle,而不会产生一个新的event,这时候GetLastError会传回ERROR_ALREADY_EXISTS.
常用的操作event的函数及其说明如下:
函数 | 说明 |
SetEvent | 把event对象设为激发状态 |
ResetEvent | 把event对象设为非激发状态 |
PulseEvent | 如果是一个Manual Reset Event:把event对象设为激发状态,唤醒“所有”等待中的线程,然后event恢复为非激发状态。如果是一个Auto Reset Event:把event对象设为激发状态,唤醒“一个”等待中的线程,然后event恢复为非激发状态。 |
5. Interlocked函数:InterlockedIncrement和InterlockedDecrement函数,这个两个函数的返回值只能够和0做比较,不能和任何其他数值比较。另外InterlockedExchange函数可以设定一个新值并传回旧值,它提供了一个在多线程环境下的安全做法,用以完成一个很基础的运算操作。
6. 同步机制总结:
Critical Section用来实现“排他性占有”,适用范围是单一进程的各线程之间,它是一个局部对象,不是一个核心对象;快速而有效率;不能够同时有一个以上的critical section被等待;无法侦测是否已被某个线程放弃。
Mutex是一个核心对象,可以在不同的线程之间实现“排他性占有”,甚至即使那些线程分属不同进程,它是一个核心对象;如果拥有mutex的那个线程结束,则会产生一个“abandoned”错误信息;可以使用Wait…函数等待一个mutex;可以具名,因此可以被其他进程开启;只能够被拥有它的那个线程释放
Semaphore被用来追踪有限的资源,它是一个核心对象;没有拥有者;可以具名,因此可以被其他进程开启;可以被任何一个线程释放。
Event通常用于overlapped I/O,或用来设计某些自定义的同步对象,它是一个核心对象;完全在程序掌控之下;适用于设计新的同步对象;“要求苏醒”的请求并不会被储存起来,可能会遗失掉;可以具名,因此可以被其他进程开启。
四. 线程的初始化结束以及优先级的调整
1. 结束一个线程
BOOL TerminateThread(HANDLE hThread,DWORD dwExitCode)
参数:
hThread---欲令其结束的线程的handle,该线程是我们的行动目标
dwExitCode—该线程的结束代码
返回值:如果函数成功,则传回TRUE,如果失败,则传回FALSE,GetLastError可获知更多细节。
TerminateThread强迫一个线程结束,手段激烈而有力,甚至不允许该线程有任何挣扎的机会,所以该线程没有机会在结束前清理自己,该函数不会再目标线程中丢出一个异常情况,目标线程在核心层面就被根本抹杀了,目标线程没有机会捕捉所谓的“结束请求”,并从而获得清理自己的机会。而且目标线程的堆栈没有被释放掉,于是可能引起一大块内存泄露,而且,任何一个与此线程有附着关系的DLLs也都没有机会获得“线程解除附着”的通知。这个函数所带来的潜伏危机还包括:如果线程正进入一个critical section之中,该critical section将因此永远处于锁定状态,因为critical section不像mutex那样拥有“abandoned”状态。所以应尽量避免使用该函数来结束一个线程。推荐的做法是利用一个手动重置的event对象,worker线程可以检查该event对象的状态,来决定是否结束自己。
2. 线程优先权
Win32优先权是以数值表现的,并以进程的“优先权类别”、线程的“优先权层级”和操作系统当时采用的“动态提升”作为计算基准,所有因素放在一起,最后获得一个0~31的数值。
(1) 优先权类别:是进程的属性之一,这个属性可以表现出这一进程和其他进程比较之下的重要性。Win32提供4种优先权类别,每一个优先权类别对应一个基本的优先权层级。优先权类别适用于进程而非线程,操作函数为SetPriorityClass和GetPriorityClass。
优先权类别(Priority class) | 基础优先权值 |
HIGH_PRIORITY_CLASS | 13 |
IDLE_PRIORITY_CLASS | 4 |
NORMAL_PRIORITY_CLASS | 7或8 |
REALTIME_PRIORITY_CLASS | 24 |
注意类别REAL_PRIORITY_CLASS,这个类别用以协助解决一些和时间有密切关系的工作,不应该用于标准GUI程序或甚至于典型的服务器程序,可用在监控驱动程序方面。
(2)优先权层级:是对进程的优先权类别的一个修改,使你能够调整同一个进程内的各线程的相对重要性,共有7种优先权层级。可利用函数SetThreadPriority和GetThreadPriority来调整和获取优先权层级。
优先权层级(Prioriyt levels) | 调整值 |
THREAD_PRIORITY_HIGHEST | +2 |
THREAD_PRIORITY_ABOVE_NORMAL | +1 |
THREAD_PRIORITY_NORMAL | 0 |
THREAD_PRIORITY_BELOW_NORMAL | -1 |
THREAD_PRIORITY_LOWEST | -2 |
THREAD_PRIORITY_IDLE | Set to 1 |
THREAD_PRIORITY_TIME_CRITICAL | Set to 15 |
(3)动态提升:决定线程真正优先权的最后一个因素是其目前的动态提升值,所谓动态提升是对优先权的一种调整,使系统能够机动对待线程以强化程序的可用性。一般来说,拥有键盘焦点的程序的优先权得以提升+2,这个设定使得前台程序比后台程序获得较多的CPU时间,因此即使系统忙碌,前台程序还是容易保持其UI敏感度。同样优先权提升也适用于同一个进程的线程,用以反应用户的输入或磁盘的输入。(鼠标消息和计时器消息也引起优先权提升),另外还可能发生在任何一个线程身上,当该线程“等待状态”获得满足时。
3.线程的挂起与唤醒
调用函数SuspendThread,使指定的线程挂起,直到有人调用ResumeThread将其唤醒,若ResumeThread函数调用成功,返回线程的前一个挂起次数,失败则传回0XFFFFFFFF,可调用GetLastError获取更多信息。SuspendThread成功返回该线程目前的挂起次数,失败返回0XFFFFFFFF。SuspendThread的最大用途就是用来协助撰写调试器,要注意该函数可能引起的死锁问题。
五、OVERLAPPED I/O
Overlapped I/O是Win32的一种技术,你可以要求操作系统为你传送数据,并且在传送完毕时通知你,这项技术使你的程序在I/O进行过程中仍然能够继续处理事务。
(1)Win32文件操作函数
CreateFile可以用来打开各式各样的资源,包括文件(硬盘,软盘,光盘或其他),串行口和并行口,Named pipes,console等。
HANDLE CreateFile(
LPCTSTR lpFileName, //指向文件名称
DWORD dwDesiredAccess,//存取模式(读或写)
DWORD dwShareMode,//共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes,//指向安全属性结构
DWORD dwCreationDisposition,//如何产生
DWORD dwFlagsAndAttributes,//文件属性
HANDLE hTemplateFile //一个临时文件,将拥有全部的属性拷贝
)
在该函数的第六个参数中可设置FILE_FLAG_OVERLAPPED与其他值进行组合使用异步调用,Overlapped I/O的基本型式是以ReadFile和WriteFile完成的,这时你不能再调用c runtime library中的stdio.h中的C runtime函数。
BOOL ReadFile(
HANDLE hFile,//欲读取的文件句柄
LPVOID lpBuffer,//接受数据的缓冲区
DWORD nNumberOfBytesToRead,//欲读取的字节个数
LPDWORD lpNumberOfBytesRead,//实际读取的字节个数的地址
LPOVERLAPPED lpOverlapped//指针,指向overlapped info
);
BOOL WriteFile(
HANDLE hFile,//欲写入的文件句柄
LPVOID lpBuffer,//储存数据的缓冲区
DWORD nNumberOfBytesToWrite,//欲写入的字节个数
LPDWORD lpNumberOfBytesWritten,//实际写入的字节个数的地址
LPOVERLAPPED lpOverlapped//指针,指向overlapped info
);
上述两个函数类似于C runtime函数中的fread何fwrite,差别在于最后一个lpOverlapped参数,若CreateFile的第6个参数中指定FILE_FLAG_OVERLAPPED,就必须在上述函数中提供一个指向OVERLAPPED结构的指针。
(2)OVERLAPPED结构
OVERLAPPED结构执行两个功能:一,用以识别每一个目前正在进行的overlapped操作,二,它在你和系统之间提供一个共享区域,参数可以在该区域中双向传递。
typedef struct _OVERLAPPED{
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
}OVERLAPPED,*LPOVERLAPPED;
成员名称 | 说明 |
Internal | 通产它被保留,然而当GetOverlappedResult返回FALSE,且GetLastError并非传回ERROR_IO_PENDING时,这个栏位将内含一个视系统而定的状态。 |
InternalHigh | 通常它被保留,然而当GetOverlappedResult传回TRUE时,这个栏位将内含“被传输数据的长度”。 |
Offset | 文件中开始被读取或写入的偏移位置(以字节为单位),该偏移位置从文件头开始算起,如果目标设备(如pipes)并没有支持文件位置,此栏位被忽略。 |
OffsetHigh | 64位的文件偏移位置中,较高的32位,如果目标设备(如PIPES)并没有支持文件位置,此栏位被忽略。 |
hEvent | 一个手动重置的event对象,当overlapped I/O完成时即被触发,ReadFileEx和WriteFileEx会忽略这个栏位,彼时它可能被用来传递一个用户自定义的指针。 |
由于OVERLAPPED结构的生命期超越ReadFile和WriteFile函数,因此该结构最好放在heap中。
(3)被激发的File Handles
如果你需要等待overlapped I/O的执行结果,可利用WaitForMultipleObjects函数,因为文件handle是核心对象,一旦操作完毕即被激发。当你操作完成之后,请调用GetOverlappedResult以确定结果如何。这个函数的价值在于,在文件操作真正完成之前,你不可能确实知道它是否成功。
BOOL GetOverlappedResult(
HANDLE hFile,
LPOVERLAPPED lpOverlapped,
LPDWORD lpNumberOfBytesTransferred,
BOOL bWait);
参数:
hFile----文件或设备的handle。
lpOverlapped----一个指针,指向OVERLAPPED结构。
lpNumberOfBytesTransferred----一个指针,指向用以表示真正被传输的字节个数。
bWait---用以表示是否要等待操作完成。TRUE表示要等待。
返回值:若操作成功,返回TRUE,否则返回FALSE,可用GetLastError获取详细信息。
虽然你要求一个overlapped操作,但它不一定就是overlapped,如果数据已经被放进cache中,或操作系统认为它可以很快地取得那份数据,那么文件操作就会在ReadFile或WriteFile返回之前完成,而这两个函数将返回TRUE,这种情况下,文件handle处于激发状态,而对文件的操作可被视为overlaped一样。另外,如果你要求一个文件操作为overlapped,而操作系统把这个“操作请求”放到队列中等待执行,那么ReadFile和WriteFile都会返回FALSE,这时你必须调用GetLastError并确定它传回ERROR_IO_PENDING,那意味着“overlapped I/O请求”被放进队列中等待执行,若返回ERROR_HANDLE_EOF,那就真正代表一个错误了。
(4)被激发的Event对象
以handle作为激发机制,一个明显的限制就是无法说出到底是哪一个overlapped操作完成了,若为每一个可能正在进行中的overlapped操作调用GetOverlappedResult,则效率很低,这时可利用OVERLAPPED结构体中的hEvent成员。注意该event对象必须是一个手动重置的,若为自动重置,系统核心可能在你有机会等待event对象之前,先激发它,而event对象的状态时不能被保存的,所以可能导致event状态遗失,从而使得Wait函数永不返回。
调用ReadFile时,由于操作系统的原因,为overlapped I/O指定的缓冲区必须在内存中锁定,如果系统或同一个程序中有太多缓冲区在同一时间锁定,Win32可能会传回ERROR_INVALID_USER_BUFFER,代表此刻没有足够的资源来处理这个“I/O请求”,此时可让线程sleep一段时间,然后再重复调用ReadFile。
(5)异步过程调用(APCs)
使用OVERLAPPED结构中的event对象,只能等待MAXIMUM_WAIT_OBJECTS个对象,此值一般为64,另外还要根据哪一个event对象被激发,而调用相应的处理过程,解决的方法就是异步过程调用,即Ex版的ReadFile和WriteFile函数,这两个函数的最后一个参数是一个回调函数地址,当一个overlapped I/O完成时,系统应该调用该函数,此函数被称作I/O completion routine。Windows调用该回调函数的时机是:你的线程必须处于所谓的“alertable”状态下。如果线程因为以下五个函数而处于等待状态,而其“alertable”标记为TRUE,则该线程就处于“alertable”状态。这五个函数为:
SleepEx;WaitForSingleObjectEx;WaitForMultipleObjectsEx;MsgWaitForMultipleObjectsEx和SignalObjectAndWait函数。只有当线程处于“alertable”状态时,APCs才会被调用!
你提供的I/O completion routine回调函数样式如下:
VOID WINAPI FileIOCompletionRoution(
DWORD dwErrorCode,
DWORD dwNumberOfBytesTransferred,
LPOVERLAPPED lpOverlapped)
参数:
dwErrorCode---这个参数内含以下的值:0表示操作完成,ERROR_HANDLE表示操作已到文件尾端。
dwNumberOfBytesTransferred----真正被传输的字节数。
lpOverlapped----指向OVERLAPPED结构,此结构由开启overlapped I/O操作的函数提供。
若利用I/O completion routine,OVERLAPPED结构中的hEvent栏位就不必放置一个event handle,而是自由运用。
对于少于32KB的数据传输请求,采用overlapped I/O会降低效率,另外有两种情况overlapped I/O总是会同步执行。一.你进行一个写入操作而造成文件的扩展;二.你读写一个压缩文件。
(6)I/O Completion Ports
APCs的问题是有些I/O API并不支持APCs,如listen和WaitCommEvent,另外只有发出“overlapped请求”的那个线程才能够提供callback函数。
I/O Completion Ports是一种特殊的核心对象,用来综合一堆线程,让它们为“overlapped请求”服务,其所提供的功能甚至可以跨越多个CPU,其好处包括:与WaitForMultipleObjects不同,不限制handles的个数;允许一个线程将一个请求暂时保存下来,而由另一个线程为它做实际服务;默默支持scalable架构(是指借着RAM或硬盘空间或CPU个数增加而能够提升应用程序性能的一种系统)。
I/O Completion Ports的操作流程:①产生一个I/O completion port;②让它和一个文件handle产生关联;③产生一堆线程;④让每一个线程在Completion Port上等待;⑤开始对着那个文件handle发出一些overlapped I/O请求。
产生一个I/O Completion Port,调用函数CrateIoCompletionPort(
HANDLE FileHandle,HANDLE ExistingCompletionPort,
DWORD CompletionKey,DWORD NumberOfConcurrentThreads)
参数:
FileHandle----文件或设备的handle,在Windows NT3.51之后,此栏位可设定为INVALID_HANDLE_VALUE,于是产生一个没有和任何文件handle有关系的port。
ExistingCompletionPort----如果此栏位被指定,那么上一栏位FileHandle就会被加到此port之上,而不会产生新的port,指定NULL可以产生一个新的port。
CompletionKey----用户自定义的一个数值,将被交给提供服务的线程,此值和FileHandle有关联。
NumberOfConcurrentThread----与此I/O completion port有关联的线程个数。
返回值:如果函数成功,则传回一个I/O completion port的handle,如果失败,则传回FALSE,GeLastError可获得更详细的失败原因。
任何文件只要附着到一个I/O completion port身上,都必须先以FILE_FLAG_OVERLAPPED开启,如果已经附着上去,就不能够再以ReadFileEx和WriteFileEx操作它。通常把NumberOfConcurrentThreads设置为0。
通常CreateIoCompletionPort被调用两次,第一次先指定FileHandle为INVALID_HANDLE_VALUE,并设定ExistingCompletionPort为NULL,用以产生一个port,然后再为每一个欲附着上去的文件handle调用一次CreateIoCompletionPort,将ExistingCompletionPort设定为第一次调用所传回的handle。
一旦completion port产生出来,必须自己以CreateThread或_beginthreadex或AfxBeginThread产生出线程,产生的合理的线程个数应该是CPU个数的两倍再加2。在completion port上等待的线程是以先进后出的次序提供服务。
Worker线程初始化自己后,它应该调用GetQueuedCompletionStatus,这个操作像是WaitForSingleObject和GetOverlappedResult的组合,函数如下:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytesTransferred,
LPDWORD lpCompletionKey,
LPOVERLAPPED* lpOverlapped,
DWORD dwMilliseconds);
参数:
CompletionPort----将在其上等待的completion port。
lpNumberOfBytesTransferred----一个指针,指向DWORD,该DWORD将收到被传输的数据字节数。
lpCompletionKey----一个指针,指向DWORD,该DWORD将收到由CreateIoCompletionPort所定义的key。
lpOverlapped----这个栏位的名称是个错误,它其实应该命名为lplpOverlapped,你应该把一个指针的地址放在上面,系统会填以一个overlapped结构的指针,该结构用以初始化I/O操作。
dwMilliseconds----等待的最长时间(毫秒),如果时间终了,lpOverlapped将被设为NULL,而函数传回FALSE。
返回值:
如果函数成功地将一个completion packet从队列中取出,并完成一个成功的操作,函数将传回TRUE,并填写由lpNumberOfBytesTransferred,lpCompletionKey和lpOverlapped所指向的变量内容。
如果操作失败,但completion packet已经从队列中取出,则函数传回FALSE,lpOverlapped指向失败之操作,调用GetLastError可获知原因。
如果函数失败,则传回FALSE,并将lpOverlapped设为NULL,调用GetLastError可获知原因。
下面这些函数调用可以启动“能够被一个I/O completion port掌握”的I/O操作。ConnectNamePipe;DeviceIoControl;LockFileEx;ReadFile;TransactNamePipe;WaitCommEvent;WriteFile。为了使用completion port,主线程或任何其他线程可以对着一个与此completion port有关联的文件,进行读,写或其他操作,该线程不需要调用WaitForMultipleObjects,因为池子里的各个线程都曾经调用过GetQueuedCompletionStatus,一个I/O操作完成,一个等待中的线程将会自动被释放,以服务该操作。
有时文件以overlapped I/O状态打开,进行简单的读写,当写入操作完成时,I/O completion port将收到一个packet中,但是我们并不想使每个操作均引发completion port通告。我们可以进行一个I/O操作,却不引发I/O completion packet被送往completion port,我们可以以旧有的受激发的event对象机制取而代之,为了这么做,必须设定一个OVERLAPPED结构,内含一个合法的手动重置的event对象,放在hEvent栏位,然后把该handle的最低位设为1.
OVERLAPPED overlap;
HANDLE hFile;
char buffer[128];
DWORD dwBytesWritten;
Memset(&overlap,0,sizeof(OVERLAPPED));
Overlap.hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
Overlap.hEvent = (HANDLE)((DWORD)overlap.hEvent | 0x1);
WriteFile(hFile,buffer,128,&dwBytesWritten,&overlap);
六.数据一致性
typedef struct _RWLock
{
// Handle to a mutex that allows
// a single reader at a time access
// to the reader counter.
HANDLE hMutex;
// Handle to a semaphore that keeps
// the data locked for either the
// readers or the writers.
HANDLE hDataLock;
// The count of the number of readers.
// Can legally be zero or one while
// a writer has the data locked.
int nReaderCount;
} RWLock;
//
// Reader/Writer prototypes
//
BOOL InitRWLock(RWLock *pLock);
BOOL DestroyRWLock(RWLock *pLock);
BOOL AcquireReadLock(RWLock *pLock);
int ReleaseReadLock(RWLock *pLock);
BOOL AcquireWriteLock(RWLock *pLock);
int ReleaseWriteLock(RWLock *pLock);
BOOL ReadOK(RWLock *pLock);
BOOL WriteOK(RWLock *pLock);
BOOL FatalError(char *s);
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include "ReadWrit.h"
// If we wait more than 2 seconds, then something is probably wrong!
#define MAXIMUM_TIMEOUT 2000
// Here's the pseudocode for what is going on:
//
// Lock for Reader:
// Lock the mutex
// Bump the count of readers
// If this is the first reader, lock the data
// Release the mutex
//
// Unlock for Reader:
// Lock the mutex
// Decrement the count of readers
// If this is the last reader, unlock the data
// Release the mutex
//
// Lock for Writer:
// Lock the data
//
// Unlock for Reader:
// Unlock the data
///
BOOL MyWaitForSingleObject(HANDLE hObject)
{
DWORD result;
result = WaitForSingleObject(hObject, MAXIMUM_TIMEOUT);
// Comment this out if you want this to be non-fatal
if (result != WAIT_OBJECT_0)
FatalError("MyWaitForSingleObject - Wait failed, you probably forgot to call release!");
return (result == WAIT_OBJECT_0);
}
BOOL InitRWLock(RWLock *pLock)
{
pLock->nReaderCount = 0;
pLock->hDataLock = CreateSemaphore(NULL, 1, 1, NULL);
if (pLock->hDataLock == NULL)
return FALSE;
pLock->hMutex = CreateMutex(NULL, FALSE, NULL);
if (pLock->hMutex == NULL)
{
CloseHandle(pLock->hDataLock);
return FALSE;
}
return TRUE;
}
BOOL DestroyRWLock(RWLock *pLock)
{
DWORD result = WaitForSingleObject(pLock->hDataLock, 0);
if (result == WAIT_TIMEOUT)
return FatalError("DestroyRWLock - Can't destroy object, it's locked!");
CloseHandle(pLock->hMutex);
CloseHandle(pLock->hDataLock);
return TRUE;
}
BOOL AcquireReadLock(RWLock *pLock)
{
BOOL result = TRUE;
if (!MyWaitForSingleObject(pLock->hMutex))
return FALSE;
if(++pLock->nReaderCount == 1)
result = MyWaitForSingleObject(pLock->hDataLock);
ReleaseMutex(pLock->hMutex);
return result;
}
BOOL ReleaseReadLock(RWLock *pLock)
{
int result;
LONG lPrevCount;
if (!MyWaitForSingleObject(pLock->hMutex))
return FALSE;
if (--pLock->nReaderCount == 0)
result = ReleaseSemaphore(pLock->hDataLock, 1, &lPrevCount);
ReleaseMutex(pLock->hMutex);
return result;
}
BOOL AcquireWriteLock(RWLock *pLock)
{
return MyWaitForSingleObject(pLock->hDataLock);
}
BOOL ReleaseWriteLock(RWLock *pLock)
{
int result;
LONG lPrevCount;
result = ReleaseSemaphore(pLock->hDataLock, 1, &lPrevCount);
if (lPrevCount != 0)
FatalError("ReleaseWriteLock - Semaphore was not locked!");
return result;
}
BOOL ReadOK(RWLock *pLock)
{
// This check is not perfect, because we
// do not know for sure if we are one of
// the readers.
return (pLock->nReaderCount > 0);
}
BOOL WriteOK(RWLock *pLock)
{
DWORD result;
// The first reader may be waiting in the mutex,
// but any more than that is an error.
if (pLock->nReaderCount > 1)
return FALSE;
// This check is not perfect, because we
// do not know for sure if this thread was
// the one that had the semaphore locked.
result = WaitForSingleObject(pLock->hDataLock, 0);
if (result == WAIT_TIMEOUT)
return TRUE;
// a count is kept, which was incremented in Wait.
result = ReleaseSemaphore(pLock->hDataLock, 1, NULL);
if (result == FALSE)
FatalError("WriteOK - ReleaseSemaphore failed");
return FALSE;
}
///
/*
* Error handler
*/
BOOL FatalError(char *s)
{
fprintf(stdout, "%s\n", s);
// Comment out exit() to prevent termination
exit(EXIT_FAILURE);
return FALSE;
}
七.C Run-Time Library
如果写一个多线程程序,并且不使用MFC,那么应该总是和多线程版本的C Runtime Library链接,并且总是以_beginthreadex和_endthreadex取代CreateThread和ExitThread。_beginthreadex的参数和CreateThread一样,并且承担适度的C Runtime library初始化工作。
unsigned long _beginthreadex(
void* security,
unsigned stack_size,
unsigned (_stdcall *start_address)(void*),
void* arglist,
unsigned initflag,
unsigned* thraddr);
参数:
security----相当于CrateThread中的security参数,NULL表示使用默认的安全属性,Win95会忽略此参数,对应的Win32数据类型是LPSECURITY_ATTRIBUTES.
stack_size----新线程的堆栈大小,单位是字节,对应的Win32数据类型是DWORD.
start_address----线程启动时所执行的函数,对应的Win32数据类型是LPTHREAD_START_ROUTINE.
arglist----新线程将收到的一个指针,这个指针只是单纯地被传递过去,runtime library并没有对它做拷贝操作,对应的Win32数据类型是LPVOID.
initflag----启动时的状态标记,对应的Win32数据类型是DWORD.
thrdaddr----新线程的ID将借此参数传回,对应的Win32数据类型是LPDWORD.
返回值:传回线程的handle,此值必须被强制转换为Win32的HANDLE后才能使用,如果函数失败,传回0,而其原因将被设定在erro和doserro全局变量中。
由于_beginthread调用CreateThread,所以必须对_beginthread的返回值调用CloseHadle。
与ExitThread对应的c tuntime library函数名为_endthreadex,该函数可以被任意线程在任意时间调用,它需要一个表示线程返回代码的参数,事实上,当线程的startup函数返回时,_endthreadex会自动被runtime library调用。绝对不能在一个以_beginthreadex启动的线程中调用ExitThread,这样会导致 C runtime library没有机会释放为该线程配置的资源。
应采用多线程版的C runtime library,并使用_beginthreadex和_endthreadex的情况如下:
①在C程序中使用malloc和free,或是在c++中使用new和delete
②调用stdio.h或io.h中声明的任何函数,包括fopen,open,getchar,write,printf等,所有这些函数都用到共享的数据结构以及errno,可以使用wsprintf代替。
③使用浮点变量或浮点运算函数。
④调用任何一个使用了静态缓冲区的runtime函数,如asctime,strtok或rand等函数。
为避免使用stdio.h,须解决三个问题:
(1)通过使用wsprintf来解决字符串格式化问题,取代sprintf函数。
(2)利用函数GetStdHandle获得取代C Runtime library中stdin,stdout,stderr的东西。
HANDLE GetStdHandle(
DWORD nStdHandle)
参数:
nStdHandle----设定传回handle的型态,必须是以下三者之一:
STD_INPUT_HANDLE;STD_OUTPUT_HANDLE;
STD_ERROR_HANDLE
(3)利用Console API控制屏幕,Console APT提供对光标的控制,以及对字符属性,窗口标题,鼠标等的控制。
为适当清除C Runtime library中的结构,对于以_beginthread或_beginthreadex产生新线程的程序,应使用以下两种技术结束程序:
(1)调用C runtime library中的exit函数
(2)从main返回系统
任何一种情况下,runtime library都会自动进行清理操作,最后调用ExitProcess,以上方法均不会等待线程的结束,任何正在运行的线程都会被自动终止。不到万不得已不可调用abort函数,因为调用该函数其间没有任何退出程序可以调用,文件缓冲区也没有清理。
关于_beginthread函数
unsigned long _beginthread(
void (_cdecl* start_address)(void*),
unsigned stack_size,
void* arglist)
参数:
Start_address----线程的起始函数,注意调用方式。
Stack_size----堆栈大小,以字节为单位,与CreateThread相同,此值若为0,表示使用默认大小。
Arglist----指向一块数据,新线程将收到此指针,runtime library不会为该数据另外拷贝一份。
返回值:如果失败,返回-1,否则传回一个unsigned long,代表新线程的handle,这个handle也许可用,也许不可用,调用端不见得可以安全地使用该handle,因为利用该函数产生出来的线程所做的第一件事就是关闭自己的handle,没有这个handle,也就没有办法等待这个线程的结束,改变其参数或取得其结束代码。对应的线程结束函数是:
_endthread()不能指定结束代码,对着一个以_beginthread产生出来的线程调用GetExitCodeThread是没有意义的。
八.C++设计安全的多线程类
Class CLockableObject
{
Public:
CLockableObject(){}
virtual ~CLockableObject(){}
virtual void Lock() = 0;
virtual void Unlock() = 0;
};
Class CriticalSection:public CLockableObject
{
public:
CriticalSection();
{
InitializeCriticalSection(&m_CritSect);
}
virtual ~CriticalSection()
{
DeleteCriticalSection(&m_CritSect);
}
virtual void Lock()
{
EnterCriticalSection(&m_CritSect);
}
virtual void Unlock()
{
LeaveCriticalSection(&m_CritSect);
}
private:
CRITICAL_SECTION m_CritSect;
};
class Clock
{
public:
Clock(CLockableObject* pLockable)
{
m_pLockable = pLockable;
m_pLockable->Lock();
}
~Clock()
{
m_pLockable->Unlock();
}
pirvate:
CLockableObject* m_pLockable;
};
使用举例:
Class StringV
{
Public:
StringV()
{
m_pData = NULL;
}
~StringV()
{
delete[] m_pData;
}
Void Set(char* str)
{
Clock localLock(&m_Lockable);
delete[] m_pData;
m_pData = NULL;
m_pData = new char[::strlen(str) + 1];
::strcpy(m_pData,str);
}
private:
CriticalSection m_Lockable;
char* m_pData;
}
九.MFC多线程
如果在MFC程序中产生一个线程,而该线程将调用MFC函数或使用MFC的任何数据,那么你必须以AfxBeginThread或CWinThread::CreateThread来产生这些线程。
产生work线程的AfxBeingThread版本
CWinThread* AfxBeingThread(
AFX_THREADPROC pfnThreadProc,
LPVOID pParam,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL)
参数:
pfnThreadProc----函数名称,用来启动线程
pParam----任意4字节数值,用来传给新线程,它可以是整数或指针或为0
nPriority----新线程的优先权,如果是缺省值,表示新线程的优先权将与目前优先权相同。
nStackSize----新线程的堆栈大小,0表示使用默认的堆栈大小。
dwCreateFlags----此值必须为0或CREATE_SUSPENDED,如果为0表示立刻开始新线程的生命。
lpSecurityAttrs----新线程的安全属性,在Win95中忽略。
返回值:失败返回NULL,成功返回指向新产生出来的CWinThread对象的指针。
CWinThread中有个成员变量m_bAutoDelete,这个参数可以阻止CWinThread对象被自动删除,为了能够设定此变量而不产生一个死锁,必须先以挂起状态产生线程。
产生UI线程的AfxBeingThread版本
CWinThread* AfxBeginThread(
CRuntimeClass* pThreadClass,
int nPriority = THREAD_PRIORITY_NORMAL,
UINT nStackSize = 0,
DWORD dwCreateFlags = 0,
LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL)
参数:
pThreadClass---指向你所产生的一个类的runtime class,该类派生自CWinThread。
nPriority----新线程的优先权,如果是缺省值,表示新线程的优先权将与目前优先权相同。
nStackSize----新线程的堆栈大小,0表示使用默认的堆栈大小。
dwCreateFlags----此值必须为0或CREATE_SUSPENDED,如果为0表示立刻开始新线程的生命。
lpSecurityAttrs----新线程的安全属性,在Win95中忽略。
返回值:失败返回NULL,成功返回指向新产生出来的CWinThread对象的指针。
利用vc向导产生一个类CDemoThread,派生自CWinThread,使用AfxBeginThread产生新的UI线程。
CDemoThread* pThread = (CDemoThread*)AfxBeginThread(
RUNTIME_CLASS(CDemoThread));
一旦线程开始执行,MFC将调用你的派生类中的InitInstance成员函数,并进入消息循环中,你的CWinThread派生类的消息映射表将被作为消息派送的指引。
MFC各对象和Win32 handle之间的映射关系记录在线程局部存储中,因此没有办法把一个MFC对象从某线程交到另一线程手上,也不能够在线程之间传递MFC对象指针,这些对象包括CWnd,CDC,CPen,CBrush,CFont,CBitmap,CPalette等等。替代方案是:不要放置MFC对象,改放对象的handle,利用GetSafeHandle或GetSafeHwnd;当把handle传递给线程时,线程可以把该handle附着到一个新的MFC对象,使用FromHandle可以产生一个临时对象,使用Attach可以附着到一个永久对象,但在退出之前应Detach。但是想在另一线程中调用比如UpdateAllViews这种函数,那么只能发送一个用户自定义消息,回到原线程中。另外出于效率的考虑,MFC中的CString类并没有实现安全应用于线程的锁定操作,要让CString安全应用于多线程,应自己实现锁定。
十.GDI与窗口管理
在Win16中,所有窗口共享同一个消息队列,而在Win32中每一个线程有自己专属的消息队列。注意在Win32中,所有传送给某一窗口的消息,将由产生该窗口的线程负责处理。
| SendMessage | PostMessage |
同一线程 | 直接调用窗口函数 | 把消息放到消息队列中然后立刻返回。 |
不同的线程 | 切换到新线程中并调用窗口函数,在该窗口函数结束之前,SendMessage不会返回。 | 立刻返回,消息则被放到另一个线程的消息队列中。 |
当一个线程(waiting thread)调研那个SendMessage向另一个线程(destination thread)发送消息,而SendMEssage尚未返回时,waiting thread依然可以处理来自SendMessage的消息,但不处理其他消息。为解决这个问题,当destination thread调用以下任何函数时,waiting thread必须自动醒来。这些函数包括:DialogBox;DialogBoxIndirect; DirlogBoxIndirectParam;DialogBoxParam;GetMessage;MessageBox;
PeekMessage。或者为了让waiting thread能够继续工作,在destination thread中可以调用ReplyMessage函数。该函数允许destination thread窗口函数完成之前,waiting thread可继续进行。另外可借助函数SendMessageTimeOut,它允许你指定一个时间,时间终了后不管对方如何,一定返回。SendMessageCallback,该函数将指定的消息发送到一个或多个窗口。此函数为指定的窗口调用窗口程序,并立即返回。当窗口程序处理完消息后,系统调用指定的回调函数,将消息处理的结果和一个应用程序定义的值传给回调函数。如果发送到当前线程中,则直接调用窗口过程,发送到其它线程中则立即返回。
BOOL PostThreadMessage(
DWORD idThread,
UINT Msg,
WPARAM wParam,
LPARAM lParam)
参数:
idThread----线程ID,这个ID可由GetCurrentThreadId或CreateThread获得。
Msg----消息识别代码
wParam----消息的wParam。
lParam---消息的lParam。
返回值:如果消息被成功地post,返回TRUE,否则返回FALSE,可利用GetLastError获得失败原因。
如果在一个worker线程中调用GetMessage,该线程就会产生消息队列,纵然它并没有窗口,这时就可以调用PostThreadMessage给该worker线程发送消息。
PostThreadMessage把消息post给一个线程,而非一个窗口,如果收受端尝试获取目标窗口的handle,会得到NULL,使用PostThreadMessage在不同进程之间传递消息,必须使用WM_COPYDATA消息,这样一来数据才能从一个地址空间中被映射到另一个地址空间。
十一、进程通信
(1)以消息进行通信:Windows为进程间通信专门定义了一个消息,名为WM_COPYDATA,专门用来在线程间搬移数据----不管两个线程是否属于同一个进程。WM_COPYDATA使用方式如下:SendMessage(hwndReceiver,WM_COPYDATA,(WPARAM)hwndSender,(LPARAM)&cds);其中cds为一种特殊的数据结构:
typedef struct tagCOPYDATASTRUCT{
DWORD dwData;//用户自定义值,通常用作一个行动代码,指示//lpData中的内容的用途。
DWORD cbData;//lpData所指数据大小,以字节为单位。
PVOID lpData//一块数据,可以被传送到接收端(目标窗口所属线程)。
}
对于WM_COPYDATA消息,只能用SendMessage,而不能使用PostMessage或任何其他变种函数如PostThreadMessage等。绝对不可以把“指向某一拥有虚函数的对象”的指针当做lpData来传递,因为vtbl指针将错误地指向别的进程中的函数。WM_COPYDATA是唯一一个可以在16位和32位程序之间搬移数据的方法。
(2)共享内存。
HANDLE CreateFileMapping(HANDLE hFile,
LPSECURITY_ATTRIBUTES lpFileMappinAttributes,DWORD flProtect, DWORD dwMaximumSizeHigh,DWORD dwMaximumSizeLow,LPCTSTR lpName)
参数:
hFile----这个参数正常而言应该是CreateFile传回来的一个有关文件的handle,用以告诉系统将它映射到内存中。然而如果指定此参数为0xffffffff,我们就可以使用页面文件中的一块空间,取代一般的文件。
lpFileMappinAttributes----安全属性,在Win95中忽略。
flProtect----文件的保护属性,可以是PAGE_READONLY或PAGE_READWRITE或PAGE_WRITECOPY,针对跨进程的共享内存,你应该指定此参数为PAGE_READWRITE.
dwMaximumSizeHigh----映射的文件大小的高32位,如果使用页面文件,此参数将总是0,因为页面文件没有大到足够容纳4GB的共享内存空间。
dwMaximumSizeLow----映射区域的低32位,对于共享内存而言,此值应该是你要共享的内存大小。
lpName----共享内存区域的名称,任何进程或线程都可以根据这个名称,引用到这个文件映射对象,如果要产生共享内存,此参数不应该设为NULL。
返回值:调用成功返回handle,否则返回NULL,调用GetLastError可获得失败的详细原因。
为了从共享内存中获取指向可用内存的指针,调用MapViewOfFile函数。
LPVOID MapViewOfFile(
HANDLE hFileMappingObject,
DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh,
DWORD dwFileOffsetLow,
DWORD dwNumberOfBytesToMap)
参数:
hFileMappingObject----文件映射核心对象的handle,这个调用CreateFileMapping或OpenFileMapping的返回值。
dwDesiredAccess----对共享内存而言,此值应该设为FILE_MAP_ALL_ACCESS,其他目的则使用其他设定。
dwFileOffsetHigh----映射文件的高32位偏移值,如果使用页面文件,该参数应总是为0,因页面文件不能容纳4GB共享内存区域。
dwFileOffsetLow----映射文件的低32位偏移值,对于共享内存而言,该参数应总是为0,以便映射整个共享区域。
dwNumberOfBytesToMap----真正要被映射的字节数量,如果指定为0,表示要映射整个空间,所以对于共享内存而言,最简单的做法是将此参数指定为0.
返回值:成功则返回指向被映射出来的“视图”的起始地址的指针,否则返回NULL,调用GetLastError找出原因。
HANDLE OpenFileMapping(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName)
参数:
dwDesiredAccess----对于共享内存,此值应为FILE_MAP_ALL_ACCESS,其他值适用于其他目的。
bInheritHandle----如果是TRUE,表示该handle可被子进程继承。
lpName----共享内存的名称,应该有另一个进程以相同的名称调用CreateFileMapping。
返回值:成功返回一个handle,失败返回NULL,可调用GetLastError获取失败信息。
一旦完成了对共享内存的操作,应调用UnmapViewOfFile,交出由MapViewOfFile所获得的指针,然后再调用CloseHandle关闭file-mapping对象的handle。
BOOL UnmapViewOfFile(
LPVOID lpBaseAddress)
参数:
lpBaseAddress----指针,指向共享内存,这个值必须符合MapViewOfFile的返回值。
返回值:成功返回TRUE,失败返回FALSE,可调用GetLastError获取失败信息。
注意一个特殊的指针修饰词,_based,该修饰词允许指针被定义为从某一点开始起算的32位偏移值,而不是在内存中的绝对位置。例如以指针P为基地址的指针。
int _based(P) *lpHead;
举例如下:
extern SharedBlock* gpSharedBlock;//全局变量
struct SharedBlock
{
short m_nStringCount;
char __based( gpSharedBlock ) *m_pStrings[1];
};
void CShareMemDlg::OnWrite()
{
// Make sure the shared memory is available
::WaitForSingleObject(ghDataLock, INFINITE);
CEdit* pEdit = (CEdit*)GetDlgItem(IDC_EDIT);
ASSERT_VALID(pEdit);
int iLineCount = pEdit->GetLineCount();
gpSharedBlock->m_nStringCount = iLineCount;
char *pTextBuffer =
(char *)gpSharedBlock
+ sizeof(SharedBlock)
+ sizeof(char __based(gpSharedBlock) *) * (iLineCount-1);
char szLineBuffer[256];
while (iLineCount--)
{
pEdit->GetLine(iLineCount, szLineBuffer, sizeof(szLineBuffer)); szLineBuffer[pEdit->LineLength(pEdit->LineIndex(iLineCount))] = '\0';
strcpy(pTextBuffer, szLineBuffer);
gpSharedBlock->m_pStrings[iLineCount] =
(char _based(gpSharedBlock) *)pTextBuffer;
pTextBuffer += strlen(szLineBuffer) + 1;
}
::ReleaseMutex(ghDataLock);
}
共享内存使用注意事项:
(1)不要把C++ collection classes放到共享内存中
(2)不要把拥有虚函数的c++类对象放到共享内存中
(3)不要把CObject派生类的MFC对象放到共享内存中
(4)不要使用“point within the shared memory”的指针
(5)不要使用“point outside of shraed memory”的指针
(6)使用“based”指针是安全的,但要小心使用。
十二、动态链接库
BOOL WINAPI DllMain(
HANDLE hinstDLL,DWORD fdwReason,LPVOID lpReserved)
参数:
hinstDLL----这个DLL的module handle。
fdwReason----DllMain被调用的原因,可能是以下之一:
DLL_PROCESS_ATTACH,DLL_PROCESS_DETACH
DLL_THREAD_ATTACH,DLL_THREAD_DETACH
lpReserved----提供更多信息以补充fdwReason,如果fdwReason是DLL_PROCESS_ATTACH,那么lpReserved为NULL表示DLL是被LoadLibrary载入,non—NULL表示DLL是被隐式载入,也就是在链接时期以import library和程序链接在一起。
返回值:如果fdwReason是DLL_PROCESS_ATTACH,那么DllMain应该在成功时返回TRUE,失败时返回FALSE,如果DLL是被隐式链接而DllMain返回的是FALSE,程序将没有办法执行下去,如果DLL是被显式链接而DllMain返回FALSE,那么LoadLibrary返回FALSE。如果fdwReason不是DLL_PROCESS_ATTACH,那么返回值会被忽略。
任何时候当一个进程载入或卸载一个DLL时,DllMain会被调用,线程也是一样,当一个进程开始时,它所用到得每一个DLL的DllMain都会被系统调用,并获得DLL_PROCESS_ATTACH消息,如果是线程开始执行,进程所用到的每一个DLL的DllMain也都会被系统调用,并获得DLL_THREAD_ATTACH消息。注意每个进程的第一个线程调用DllMain时,是以DLL_PROCESS_ATTACH调用,所有后续的线程是以DLL_THREAD_ATTACH调用。
函数DisableThreadLibrary可以抑制DllMain中DLL_THREAD_ATTACH和DLL_THREAD_DETACH消息。
BOOL DisableThreadLibrary(HMODULE hLibModule);
hLibModule----DLL的module handle。
返回值:成功返回TRUE,失败返回FALSE,可调用GetLastError获知详细信息,如果你指定的DLL使用了线程局部存储,这个函数调用一定会失败。
如果DllMain收到DLL_PROCESS_ATTACH后,却返回FALSE时,DllMain还是会收到DLL_PROCESS_DETACH。如果进程调用LoadLibrary时,有一个以上的线程正在运行,那么DLL_THREAD_ATTACH不会针对每一个线程送出,而是只有调用LoadLibrary的那个线程会发出。DllMain不接收任何因TerminateThread而结束的线程DLL_THREAD_DETACH通告消息。
一个使用MFC的DLL,拥有它自己的CWinThread对象,那可视为CWinApp对象的一部分。当DLL接收到DLL_PROCESS_ATTACH时,MFC会调用InitInstance,当DLL接收到DLL_PROCESS_DETACH时,MFC会调用CWinThread::ExitThread,你可以提供自己的两个函数,因为它们都是虚函数,然而没有任何虚函数在DLL_THREAD_ATTACH和DLL_THREAD_DETACH发生时调用。
TLS是一种机制,通过这种机制,线程可以持有一个指针,指向它自己的一份数据结构拷贝,C runtime library和MFC都是用TLS,C runtime library把errno和strtok指针放在TLS中,MFC通过TLS来追踪每一个线程所使用的GDI对象和USER对象。这些对象只能够用于产生它们的那些线程中,关于这点MFC是斤斤计较的,通过使用TLS,MFC就可以验证对象是不是在线程间传递。
TLS的运行机制:系统中的每一个进程都有一个位数组,位数组的成员是一个标志,每个标志的值被设为FREE或INUSE,指示了此标志对应的数组索引是否在使用中,Windows保证至少有TLS_MINIMUM_AVAILABLE(在目前的操作系统中该值至少为64)个标志位可用。当一个线程被创建时,Windows会在进程地址空间中为该线程分配一个长度为TLS_MINIMUM_AVAILABLE的由4字节槽(slots)所组成的数组,数组成员的值都被初始化为0,在内部,系统将此数组和该线程联系起来,保证只能在该线程中访问此数组中的数据,每个线程都有它自己的数组,数组成员可以存储任何数据。在调用TlsAlloc时,返回值是进程位数组的一个索引,这个位数组的唯一用途就是记忆哪一个下标在使用中,通过调用TlsAlloc,系统挨个检查该位数组中成员的值,直到找到一个值为FREE的成员,把找到的成员由FREE改为INUSE后,函数返回该索引,若找不到返回TLS_OUT_OF_INDEXES,表示失败。返回的这个索引被此进程中的每一个正在运行的和以后要被创建的线程保存起来,通过函数TlsSetValue和TlsGetValue访问各自线程中的成员的值。
除了TLS之外,VC允许一个变量或结构被声明为具有线程局部性。_declspec(thread) DWORD 变量名,如果这样声明一个变量的话,该变量对每一个线程是独一无二的。每一个以这种方式声明对象的EXE或DLL,将在可执行文件中有一个特殊的节区,内含所有的线程局部变量,当EXE或DLL被载入时,操作系统会认识这个节区并适当处理,这个节区会被操作系统自动设定为“对每一个线程具有局部性”。
Tls…函数和_declspec(thread)并不冲突,可以安全地混用。一个DLL如果使用了_declspec(thread),就没有办法被LoadLibrary载入。
若要使DLL能够安全地在多线程环境中使用,需注意:
(1)不要使用全局变量,用来储存TLS槽者例外。
(2)不要使用静态变量。
(3)如有必要,尽量使用TLS。
(4)如有必要,尽量使用你的堆栈。