6.3.1 事件对象
事件对象(Events)是同步对象之一,一个事件对象就类似一种特定的短消息,它被用来通知某个线程发生了一个特定的事件。有的时候也会告诉这个线程应该做什么事情了。比如,当系统的即插即用管理器发现了一个新插入的设备的时候,它就会设定一个特定的事件,这样系统就会触发和该事件相关的线程,并告诉它系统已经添加了一个新的设备。
事件对象有信号(signaled)和无信号(nonsignaled)两种状态。事件对象在创建的时候可以选择自动的从signaled状态恢复到nonsignaled状态,或者手动恢复到这个状态。事件对象是可以被用于共享进程间的同步通讯的。
我们可以通过下面这个函数来创建事件对象:
HANDLE CreateEvent(LPSECURITY_ARRRIBUTES,lpEventAttributes,
BOOL bManualReset,BOOL bInitialState,
LPTSTR lpName);
各个参数的含义为:
l lpEventAttributes:参数lpEventAttributes表示安全属性,和前面介绍的函数一样,在Windows CE中,这个参数应该被设置成NULL。
l bManualReset:参数bManualReset表示是否手动设置事件对象的状态。这个值如果为false,表示等待函数接到信号以后,返回的此次事件对象被自动的设置成无信号状态;如果其值被设置成true,则表示等待函数接到信号以后,返回的事件不会被自动的设置成无信号状态。
l bInitialState:参数bInitialState表示事件对象初始化的时候是否被设置成信号状态或者无信号状态。
l lpName:参数lpName表示事件对象的名字。当2个进程采用了同一个名字以后,进程就可以共享同一个对象。如果我们不想设置事件对象的名称,只需要把这个参数设置成NULL即可。
需要注意的是,如果我们想在进程之间共享一个事件对象,那么就需要在每一个进程里面创建一个事件对象,而不能只在一个里面创建,然后把事件对象的句柄传递给另一个进程。
如果我们想知道通过CreateEvent函数创建的事件对象是新被创建的,还是仅仅打开了一个已经存在的事件对象,我们可以在这个函数的后面添加上GetLastError函数。如果这个函数返回ERROR_ALREADY_EXISTS,那么表示打开的是一个已经存在的事件对象。
再让我们看看下面这两个函数:
BOOL SetEvent(HANDLE hEvent);//将信号置为有效
BOOL PulseEvent(HANDLE hEvent);//先将事件置为信号态,再置为无效。
BOOL ResetEvent(HANDLE hEvent);//对于人工重置的事件,必须用这个。将信号置为无效
函数SetEvent()的作用为将事件对象设置为有信号状态。PulseEvent()的作用为将事件对象设置成有信号状态以后,立即设置为无信号状态。如果我们想手动地把事件对象设置成无信号状态,则需要使用ResetEvent()函数。
这些函数看起来重叠,我们来复习一下。一个事件对象可以被创建为自动重置或者人工重置。
如果是自动重置类型,调用SetEvent函数就可以将事件对象置为信号态。在等待该事件的线程解除阻塞之后,该事件接着自动重置到非信号态。并且一个自动重置事件在没有线程等待时,将保持为信号态,直到有一个线程来等待他。当第一个线程等待这个事件,线程立刻解除阻塞,事件也被自动重置。自动重置事件不需要PulseEvent和ResetEvent函数。 但是如果一个事件对象被创建为手工重置,ResetEvent函数就是显然必要的。
PulseEvent函数将事件置为信号态,然后重置这个事件(置为无效),使所有等待这个事件的线程都解除阻塞。
在手动重置事件上使用PulseEvent函数可以将所有等待该事件的线程都解除阻塞。在自动重置事件上使用PulseEvent函数只能解除一个线程的阻塞状态,即使有很多线程在等待这个事件。
在手动重置事件上使用SetEvent函数将事件的对象被置为有信号状态时,任意数量的等待中线程,以及随后开始等待的线程均会被释放。在自动重置事件上使用SetEvent函数将事件的对象置为有信号状态时,只能解除一个线程的阻塞状态。
备注:
调用CreateEvent函数返回的句柄,该句柄具有EVENT_ALL_ACCESS权限去访问新的事件对象,同时它可以在任何有此事件对象句柄的函数中使用。
在调用的过程中,所有线程都可以在一个等待函数中指定事件对象句柄。当指定的对象的状态被置为有信号状态时,单对象等待函数将返回。
对于多对象等待函数,可以指定为任意或所有指定的对象被置为有信号状态。当等待函数返回时,等待线程将被释放去继续运行。
初始状态在bInitialState参数中进行设置。使用SetEvent函数将事件对象的状态置为有信号状态。使用ResetEvent函数将事件对象的状态置为无信号状态。
当一个手动复原的事件对象的状态被置为有信号状态时,该对象状态将一直保持有信号状态,直至明确调用ResetEvent函数将其置为无符号状态。
当事件的对象被置为有信号状态时,任意数量的等待中线程,以及随后开始等待的线程均会被释放。
当一个自动复原的事件对象的状态被置为有信号状态时,该对象状态将一直保持有信号状态,直至一个等待线程被释放;系统将自动将此函数置为无符号状态。如果没有等待线程正在等待,事件对象的状态将保持有信号状态。
多个进程可持有同一个事件对象的多个句柄,可以通过使用此对象来实现进程间的同步。下面的对象共享机制是可行的:
·在CreateEvent函数中,lpEventAttributes参数指定句柄可被继承时,通过CreateProcess函数创建的子进程继承的事件对象句柄。
·一个进程可以在DuplicateHandle函数中指定事件对象句柄,从而获得一个复制的句柄,此句柄可以被其它进程使用。
·一个进程可以在OpenEvent或CreateEvent函数中指定一个名字,从而获得一个有名的事件对象句柄。
使用CloseHandle函数关闭句柄。当进程停止时,系统将自动关闭句柄。当最后一个句柄被关闭后,事件对象将被销毁。由于Windows为他维护一个引用计数,所以要销毁一个命名事件对象,调用CloseHandle函数的次数要和CreateHandle函数的次数相同。
下面部分仅适用于WinCE6.0
应用程序可以将一个DWORD关联到一个事件上,通过调用下面函数:BOOL SetEventData(HANDLE hEvent,DWORD dwData);参数是事件句柄和需要关联的数据。任何应用程序都可以通过下面函数取得这个数据:DWORD GetEventData(HANDLE hEvent);返回值是关联到这个事件上的数据。
6.3.2 线程等待
在Windows CE中,与线程等待相关的函数主要有下面几个:
l WaitForSingleObject
l WaitForMultipleObjects
l MsgWaitForMultipleObjects
l MsgWaitForMultipleObjectsEx
当线程被其中这些函数之一阻塞时,线程进入了一个极其节能的状态,即只消耗很少的CPU处理能力和电池电力。
1.其中WaitForSingleObject的函数原型为:
DOWRD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
此函数仅当在参数列表中指定的对象处于信号态或超过了超时间隔时,该函数才返回。函数中的参数hHandle为标识的同步对象,其等待的时间为dwMilliseconds指定的值,等待时间的单位为毫秒。当我们给参数dwMilliseconds传递的值为INFINITE的时候,表示该对象要无限期的等待下去。这个函数的返回值可以为:
l WAIT_OBJECT_0:表示同步的对象成为了信号状态;
l WAIT_TIMEOUT:表示同步的对象在指定的时间内没有成为有信号状态,也就是说在超时范围内,期望的事件没有发生;
l WAIT_ABANDONED:表示拥有互斥对象的线程被终止;
l WAIT_FAILED:表示这个函数调用失败。
2.等待进程和线程
可以等待线程和进程的句柄。这些句柄在他们对应的进程或线程终止时,被置于信号态。这样一个进程仅能够监视其他进程或线程的状态,并且在他们终止后进行一些列的动作。这个方法的一个常见用法是,一个进程创建一个新的进程,然后阻塞在新创建的进程的句柄上,知道该进程终止。
3.在Windows CE中,一个线程可以等待许多个事件,当其中的任何一个事件被标识为信号状态的时候,线程的等待将中止。要想实现这个功能,我们可以使用下面这个函数:
DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles,
BOOL fWaitAll, DWORD dwMilliseconds)
其各个参数的含义为:
l nCount:参数nCount指定了同步对象的数量;
l *lpHandles:*lpHandles指向包含的所有同步对象;
l fWaitAll:当参数fWaitAll被设置成FALSE的时候,表示只要有1个同步对象处于信号状态,那么线程就被唤醒而继续运行;如果被设置成TRUE,则表示函数要等待所有同步对象全部处于信号状态的时候,线程才被唤醒;
l dwMilliseconds:dwMilliseconds指定了等待的时间。
这个函数返回的是同步对象在数组中的索引。其中 WAIT_OBJECT_0代表第一个元素;WAIT_OBJECT_0+n代表第(n+1)个元素。如果在指定的时间内没有任何同步的对象处于信号状态,那么该函数返回WAIT_TIMEOUT。如果此函数调用失败,则函数返回值为WAIT_FAILED。
下面让我们看看MsgWaitForMultipleObjects函数,其函数原型为:
DWORD MsgWaitForMultipleObjects(DWORD nCount, LPHANDLE pHandles,
BOOL fWaitAll,
DWORD dwMilliseconds,
DWORD dwWakeMask)
这个函数的功能同WaitForMultipleObjects函数类似,只是增加了一个和消息相关的,被称为唤醒掩码(dwWakeMask)的参数。这个函数不但能够等待内核对象,还可以等待指定的消息。
再让我们看看下面这个函数:
DWORD MsgWaitForMultipleObjectsEx(DWORD nCount,LPHANDLE pHandles,
DWORD dwMilliseconds,
DWORD dwWakeMasks ,DWORD dwFlags);
这个函数实际上是MsgWaitForMultipleObjects的扩展,如果一个线程在执行大量任务的同时还要相应用户的按键消息,MsgWaitForMultipleObjects函数和MsgWaitForMultipleObjectsEx函数将会起到非常大的作用。在Windows CE中,系统只支持下面几种类型的dwWakeMasks标示:
l QS_ALLINPUT:表示收到了消息;
l QS_INPUT:表示收到了一个输入消息;
l QS_KEY:表示收到了一个按键按下、释放的消息;
l QS_MOUSE:表示收到了一个鼠标移动或者单击的消息;
l QS_MOUSEBUTTON:表示收到了一个鼠标单击的消息;
l QS_MOUSEMOVE:表示收到了一个鼠标移动的消息;
l QS_PAINT:表示收到了一个WM_PANIT消息;
l QS_POSTMESSAGE:表示收到了一个POST消息;
l QS_SENDMESSAGE:表示收到了一个发送消息;
l QS_TIMER:表示收到了一个WM_TIMER消息;
dwFlags:可以为0,或者标记为MWMO_INPUTAVAILABLE。该标记可以让函数在调用时,如果消息队列里面已经有消息,函数能够立即返回。如果MWMO_INPUTAVAILABLE没有设置,MsgWaitForMultipleObjectsEx函数将等待直到一个新的被认可的消息加入到队列。MsgWaitForMultipleObjects函数相当于dwFlags = 0 的情况。
如果返回值为WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1,对应句柄数组中的对象。如果一条消息导致该函数返回,返回值为WAIT_OBJECT_0+nCount。该函数的一个使用示例:这段代码中,句柄数组只有一个元素,hSyncHandle。
fContinue = TRUE ;
while(fContinue)
{
rc=MsgWaitForMultipleObjects(1,&hSyncHandle,FALSE,INFINITE,QS_ALLINPUT);
if(rc == WAIT_OBJECT_0){
//DO WORKS AS A RESULT OF SYNC OBJECT.
}else if(rc == WAIT_OBJECT_0+1)
{
//it’s a message;
PeekMessage(&msg,hWnd,0,0,PM_REMOVE);
if(msg.message == WM_QUIT)
fContinue = FALSE ;
else {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
6.3.3 信号量
信号量机制是一种非常有效的进程同步机制,它已经被广泛地应用于各种类型的操作系统中。从前面的介绍中我们知道,事件对象(Event Object)可以处于信号状态(signaled)和非信号状态(nonsignaled)。同样,当可用资源大于0的时候,信号对象处于有信号状态;当可用资源等于0的时候,信号对象处于非信号状态。
在Windows CE中,当线程在等待信号量的时候,该线程处于锁住(blocked)状态,这种状态一直持续到可用资源大于0为止。可用资源的最大值是在创建信号量的时候被指定的,因此我们可以通过信号量来指定访问资源的线程的数量。比如当系统中有一个10个数据大小的缓冲区的时候,我们可以允许最多10个线程同时访问。当第11个线程需要访问这个被信号量保护的缓冲区的时候,这个线程将被锁住,直到其中的一个线程释放掉它的信号量为止。
我们可以通过下面的函数来创建一个信号量;
HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount, LONG lMaximumCount,
LPCTSTR lpName);
各个参数的含义为:
l lpSemaphoreAttributes:同前面介绍的函数一样,在Windows CE中,lpSemaphoreAttributes被设置成NULL。
l lInitialCount:参数lInitialCount表示当前可用资源的初始值;
l lMaximumCount:参数lMaximumCount为最大可用资源数量;
l lpName:参数lpName为对象的名字。
当参数lInitialCount的值等于0的时候,信号对象处于非信号状态,此时内核将调用等待函数的线程设置成睡眠状态;当lInitialCount的值大于0的时候,则信号对象处于信号状态,这个时候内核将调用等待函数的线程设置成运行状态,并将信号对象当前可用的资源数减1。
需要注意的是,如果2个线程通过CreateSemaphore函数创建的信号量的名称相同,那么第2个线程的CreateSemaphore函数将不创建新的信号量对象,而是返回一个指向第一个线程创建的信号量的句柄。在这种情况下,第二次调用的其他参数,lInitialCount和lMaximumCount被忽略。为了确定调用时信号量是否已经存在,可以调用GetLastError函数并且检查返回代码是否是ERROR_ALREADY_EXISTS。
当线程访问完可用资源以后,将要释放掉信号量。释放信号量的函数为:
BOOL ReleaseSemaphor(HANDLE hSemaphore,LONG lReleaseCount,
LPLONG lpPreviousCount);
各个参数的含义为:
l hSemaphore:参数hSemaphore为信号量对象的句柄;
l lReleaseCount:参数lReleaseCount为要释放的资源数,这个参数的值必须大于0;你可能觉得这个值应该总是1,有时一个线程可能需要将计数值增加不止1。
l lpPreviousCount:参数lpPreviousCount则返回原来的可用资源数。如果我们不需要知道原来的可用资源数,则可以把这个参数设置成NULL。
销毁一个信号量,调用CloseHandle函数。如果多余一个线程创建了相同的信号量,所有的线程都必须调用CloseHandle函数,更确切的说,CloseHandle函数调用次数必须和CreateSemaphore相同,操作系统才会销毁该信号量。
当一个线程访问完可用资源以后,必须使用ReleaseSemaphor函数释放信号量,以使当前可用资源数递增。同时在Windows CE下,系统不支持OpenSemaphore函数。还有一点需要强调,等待函数在成功等待信号量对象后会把当前可用资源数-1,但是因为线程可以一次使用多个资源,因此这就有可能导致问题的出现。为了避免上面提到的问题,我们应该遵循一个线程一次只使用一个资源的原则。
6.3.4 互斥
互斥也是一种保证线程同步的手段之一,它能够保证多个线程对同一共享资源的互斥访问。只有拥有互斥对象的线程才具有访问资源的权限。同时,由于互斥对象只有一个,因此就决定了任何情况下共享资源都不会同时被多个线程所访问。同前面的信号量一样,占有资源的线程在处理完任务以后要将拥有的互斥对象交出,以保障其他线程在获得互斥对象以后可以访问资源。图6-1给出了互斥对象的工作模型。
图6-1中的方块代表共享资源,实线箭头代表拥有互斥对象的线程,虚线箭头代表不拥有互斥对象的线程。首先来看一下(1)表示的情况。此时,有3个线程需要访问共享资源,但是只有中间的一个线程拥有互斥对象,这样只有这个线程可以访问到共享资源,而其余的线程则被阻挡在外面,此时的情况如图(2)所示。当拥有互斥对象的进程访问完资源以后,会释放掉拥有的互斥对象,以便其余得到互斥对象的线程可以继续访问,这一过程如图(3)所示。
如果我们想创建一个互斥对象,可以通过下面这个函数来实现:
HANDLE CreateMutex(LPSECURITY_ATTIBUTES lpMutexAttributes,
BOOL bInitialOwner, LPCTSTR lpName);
各个参数的含义为:
l lpMutexAttributes:和前面一样,参数lpMutexAttributes必须被设置成NULL。
l bInitialOwner:参数bInitialOwner如果被设置成True,则表示当前线程占有互斥资源,互斥对象的线程ID被设置成当前线程的ID,递归计数器被设置为1,互斥对象处于无信号状态。当bInitialOwner被设置成False的时候,则表示当前线程不占有互斥资源,互斥对象包含的线程ID和递归技术器都被设置为0
l lpName:参数lpName为互斥对象的名称。
当互斥对象被创建一个,我们如果想获得这个互斥对象,首先应该通过GetLastError函数来验证互斥资源是否已经被提交,如果已经被前一个使用它的线程提交,那么我们就可以通过WaitForSingleObject函数来获得这个互斥对象。
如果想释放掉一个互斥对象,则可以通过下面这个函数来实现:
BOOL ReleaseMutex(HANDLE hMutex);
里面的参数表示互斥对象的句柄。
当一个线程在已经拥有互斥对象的同时,又调用等待函数等待同一个互斥对象的时候,等待函数会立即返回相应的值,因为这个线程已经占有了互斥对象。当一个线程被唤醒并且在访问资源资源结束以后,必须调用ReleaseMutex函数,将互斥对象的递归计数器-1。调用ReleaseMutex函数的次数必须和调用等待函数的次数相同。和Windows XP不同的是,在Windows CE中,不支持OpenMutex函数。
6.3.5 互锁函数
在Windows CE中,互锁函数的作用是保证当一个线程访问一个变量的时候,其他线程无法访问此变量,以确保变量值的唯一性,这种访问方式我们称之为原子访问。在Windows CE中,系统支持完整的互锁Win32 API函数。常见的互锁函数主要有:
LONG InterlockedIncrement(LPLONG lpAddend);
这个函数使一个LONG类型的变量增加1,里面的参数为指向这个变化变量的指针。
LONG InterlockedDecrement(LPLONG lpAddend);
这个函数使一个LONG类型的变量减少1,里面的参数为指向这个变化变量的指针。
LONG InterlockedExchange(LPLONG Target, LONG Value);
这个函数的作用是将参数Value的值赋给Target指向的值,函数将返回参数Target指向变量的初始值。
LONG InterlockedCompareExchange(LPLONG Destination, LONG Exchange,
LONG Comperand);
这个函数的作用是将参数Deatination指向的值和参数Comperand的值进行比较,如果这两个值相同,则把参数Exchange的值赋给参数Destination指向的值;如果不相同,则不变。函数返回的是参数1指向的变量的初始值。
LONG InterlockedTestExchange(LPLONG target, LONG OldValue,
LONG NewValue);
这个函数的作用是将参数target指向的值和OldValue的值进行比较,如果这两个值相同,则把参数NewValue的值赋给target指向的值;如果不相同,则不作变化。
LONG InterlockedExchangedAdd(LPLONG Addend,LONG Increment);
这个函数的作用是将参数Increment的值加到参数Addend指向的值中,函数返回的是参数Addend指向变量的初始值。
LONG InterlockedCompareExchangePointer(PVOID *Destination, PVOID Exchange,
PVOID Comperand);
这个函数的作用和函数InterlockedCompareExchange相同,只不过传递的数据都是指针类型的。函数的返回值为参数1指向指针的初始值。
LONG InterlockedExchangePointer(PVOID * Target, PVOID Value);
这个函数的作用是同函数InterlockedExchange相同,只不过传递的数据是指针类型的。函数的返回值仍旧为指针1指向指针的初始值。
6.3.6 临界区
临界区是Windows CE系统内部最常用的互斥手段。它能保证在临界区内所有被访问的资源不被其他线程访问,直到当前线程执行完临界区的代码。一般情况下,临界区对象用于保证一段程序代码的不间断性。一个临界区对象的使用通常被限定在某一个进程或者动态链接库中。
临界区和互斥量相似,有少量但是很重要的区别,临界区被限制在单个进程内,互斥量可以在进程之间共享。
与临界区相关的函数主要有:
void InitializeCriticalSection(LPCRITICAL_SECTION);
void EnterCriticalSection(LPCRITICAL_SECTION);
void LeaveCriticalSection(LPCRITICAL_SECTION);
void DeleteCriticalSection(LPCRITICAL_SECTION);
BOOL TryEnterCriticalSection(LPCRITICAL_SECTION);
这些函数的主要功能为:
l InitializeCriticalSection:函数InitializeCriticalSection的作用是初始化临界区;
l EnterCriticalSection:函数EnterCriticalSection的作用是进入临界区;
l LeaveCriticalSection:函数LeaveCriticalSection的作用是退出临界区;
l DeleteCriticalSection:函数DeleteCriticalSection的作用是撤销临界区;
l TryEnterCriticalSection:函数TryEnterCriticalSection的作用是尝试进入临界区。
上面列举的5个函数中都只有一个参数,就是LPCRITICAL_SECTION结构体变量。LPCRITICAL_SECTION结构是在WINBASE.H中定义的,我们的应用程序不需要对这个结构中的任何字段进行操作。
通过使用InitializeCriticalSection函数我们可以向系统申请一个临界区对象,在使用完以后,需要调用DeleteCriticalSection函数来释放这个临界区对象。同时,我们还要保证以EnterCriticalSection函数或者TryEnterCriticalSection函数开始,以LeaveCriticalSection函数结束的代码段中,临界区代码执行时候的相关临界区句柄必须是有效的。这样,系统就可以保证当多个线程尝试进入到同一段临界区代码的时候,只会有一个是成功的。
从上面的介绍中,我们了解到:
l 当我们需要创建临界区的时候,需要使用InitializeCriticalSection函数;
l 当线程需要进入到一个受保护的代码区域的时候,需要使用EnterCriticalSection函数;在 这里有一点需要注意的是, 该函数是把LPCRITICAL_SECTION结构作为自己的唯一参数,同时LPCRITICAL_SECTION结构是用InitializeCriticalSection函数来初始化的。
l 当线程离开临界区的时候,需要使用LeaveCriticalSection函数。在这里有一点需要注意的是,因为临界区需要跟踪我们使用的次数,因此每当我们调用一次EnterCriticalSection函数,就需要调用一次LeaveCriticalSection函数,通俗的讲,两个函数要“配对”;
l 当我们不再使用临界区的时候,需要调用DeleteCriticalSection函数,此时将会清除用来管理临界区的所有系统资源。
6.3.7 复制同步句柄
DuplicateHandle函数就是为了避免总是使用命名事件、命名互斥量和命名信号量而设计的。
从WinCE6.0开始,DuplicateHandle可以复制所有的操作系统句柄,包括文件句柄、进程 句柄以及线程句柄。之前版本的WinCE下的DuplicateHandle函数只能复制事件、互斥量和信号量句柄。