6.2.5 设置和查询线程的时间片
前面介绍过Windows CE采用基于时间片来调度具有相同优先级的线程轮换执行,每个线程默认分配的时间片大小为100 ms, Windows CE允许人工设置线程的时间片,函数CeSetThreadQuantum()实现这个功能:
BOOL CeSetThreadQuantum(
HANDLE hThread,
DWORD dwTime
);
l 参数hThread指定线程的句柄。
l 参数dwTime指定时间片大小,单位为毫秒。如果将这个值设置为0,那么线程就成为执行到结束类型线程,这意味着,一旦该线程被调度运行,只要它不被更高优先级的线程抢占,它将一直占用CPU知道线程结束为止。
如果设置线程时间片成功,返回非0,否则返回0。
查询线程的时间片信息,调用函数CeSetThreadQuantum()实现:
DWORD CeGetThreadQuantum(
HANDLE hThread
);
参数hThread指定线程句柄,返回的时间片单位为毫秒。如果调用失败,返回MAXDWORD。
6.2 6 挂起和恢复一个线程
除了被内核调度器挂起,Windows CE也允许人工挂起或恢复一个线程的执行。函数SuspendThread()将刮起指定挂起线程:
DWORD SuspendThread(
HANDLE hThread
);
参数hThread指定将被挂起的线程句柄。如果挂起线程成功,函数将返回线程的上一次挂起计数值。Windows CE内核为每个线程都维护一个挂起计数,如果挂起计数大于0,那么线程将被挂起,否则线程仍能够被调度运行。每次调用SuspendThread()函数都将使指定线程的挂起计数加1,挂起计数的最大值为MAXIMUM_SUSPEND_COUNT指定,如果一个线程的挂起计数被增加到超过这个最大值,将导致出错。
函数ResumeThread()恢复一个线程的执行,实际上这种说法是不太准确的,ResumeThread()的真正功能是将指定线程的挂起计数减1,只有当挂起计数减为0时,线程才能恢复到运行态,才可能被调度运行。这个函数的原型是:
DWORD ResumeThread(
HANDLE hThread
);
参数hThread指定线程句柄。如果函数调用成功,将返回线程的上一次挂起计数的值,否则返回0xFFFFFFFF。
如果函数返回值为1,表明线程从挂起态恢复到运行态。如果线程本身就没有被阻塞,那么将返回0,此时线程的挂起计数还保持为0,而不会再减1。
上面两个函数都是通过线程句柄来操作其它线程,使其它线程挂起或恢复。线程还可以挂起自己一段指定的时间,这通过调用Sleep()函数实现:
void Sleep(
DWORD dwMilliseconds
);
l 参数dwMilliseconds指定线程挂起的时间,单位为毫秒。如果这个参数被指定为0,表示当前线程放弃时间片,调度器将调度其它具有相同优先级的线程执行。如果没有这样的线程,那么当前线程就会又被调度执行。还可以将挂起时间设置为INFINITE,这时等同于对当前线程调用SuspendThread()函数,将导致线程的挂起计数加1,直到其它线程调用ResumeThread()才能恢复当前线程,这与在Windows桌面系统中调用Sleep(INFINITE)的效果是不同的。
6.2.7 其它线程函数
6.3 线程同步
在Windows CE7系统中 ,多个线程之间如果需要对某些共享全局的资源进行访问,就需要进行同步保护,支持的同步机制有:事件,线程等待,信号量,互斥量,互锁函数,临界区等,我们将在下面的小节分别介绍。
6.3.1 事件
事件是由Windows CE内核提供的同步对象,用于通知一个或多个线程某个特定事件发生,比如在经典的读者-写者问题中,当写者线程往缓冲区内写入数据后,可以使用事件通知读者线程进行处理。事件具有两种状态:信号态和非信号态。事件对象在被创建时可以指定为自动从信号态重置为非信号态,或者需要显示人工重置才能回到非信号态。事件还可以是命名或非命名,其中命名事件可以在不同的进程间共享,其它进程中的线程可以通过指定名字来打开已经存在的命名事件,所有的命名事件都被存储到同一个全局的队列中。在Windows CE中,使用函数CreateEvent()来创建或打开一个事件,这个函数原型为:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPTSTR lpName
);
l 参数lpEventAttributes指定事件的属性,在Windows CE中不支持,设置为NULL。
l 参数bManualReset指定在事件被置为信号态后,是否需要显示人工重置才能回到非信号态,如果为true,必须显示调用ResetEvent()函数来重置事件到非信号态;如果为false,系统将在某一个等待线程从等待函数返回后自动将事件重置到非信号态。
l 参数bInitialState指定事件的初始化状态,如果为true,事件将被初始化为信号状态,否则初始化为非信号态。
l 参数lpName用于指定信号的全局名字,如果希望创建命名信号的话。信号的名字可以包含除了反斜线符(\)外的任意字符,而且为大小写敏感。如果指定的信号名字已经存在,此时只是打开已创建的事件对象,不会再对事件进行初始化,所以参数bManualReset和参数bInitialState将被忽略,这时两个进程实际使用的是同一个事件对象,这样就能够实现进程间同步。如果参数lpName设置为NULL,将创建非命名事件。
如果创建事件对象成功,函数将返回事件的句柄,并具有EVENT_ALL_ACCESS访问权限,如果失败,将返回NULL。一旦不再使用事件,调用CloseHandle()函数来关闭事件句柄,当最后一个句柄被关闭后,系统将自动删除事件对象。
在成功创建事件对象,获取事件句柄后,可以通过下面的两个函数将事件设置为信号态:
BOOL SetEvent(
HANDLE hEvent
);
BOOL PulseEvent(
HANDLE hEvent
);
l 参数hEvent指定事件的句柄。
这两个函数的区别是,PulseEvent()函数在等待线程从等待中返回后,系统会自动将事件对象重置为非信号态,所以PulseEvent()函数只对人工重置的事件有意义,提供了更大的灵活性。
对于人工重置事件,必须显示调用ResetEvent()函数将事件重置为非信号态:
BOOL ResetEvent(
HANDLE hEvent
);
如果事件处于非信号状态,将导致调用该事件等待函数的线程被阻塞,直到事件被设置为信号状态才能继续执行,这样就实现事件同步。等待当个事件对象,可以使用WaitForSingleObject()函数,它原型如下:
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
l 参数hHandle指定等待的对象句柄,这里我们使用事件句柄。
l 参数dwMilliseconds指定等待的超时时间,单位为毫秒,在线程等待事件对象过了超时时间后,线程将返回不再等待。如果希望线程无限等待事件对象,则将该参数设置为INFINITE,直到事件对象被设置为信号态。如果设置为0,那么线程只是测试对象的状态,并马上返回,不会导致调用线程阻塞。
如果函数调用成功,可能返回三种可能的值来指示函数返回的原因:
l WAIT_FAILED: 等待失败,可能的原因是传入的对象句柄无效,当前线程被终止,系统内存不足等。
l WAIT_OBJECT_0: 等待的对象被设置为信号态
l WAIT_TIMEOUT: 等待过了超时时间,但是对象还处于非信号态。
除了用于等待事件对象,WaitForSingleObject()函数还可用于等待挥斥对象(Mutex),信号对象(Semaphore),进程对象以及线程对象。
线程还可以同时等待多个对象,通过调用WaitForMultipleObjects()函数实现,在Windows CE中,只要其中的任意一个对象被设置为信号态,就返回,函数原型为:
DWORD WaitForMultipleObjects(
DWORD nCount,
CONST HANDLE* lpHandles,
BOOL fWaitAll,
DWORD dwMilliseconds
);
l 参数nCount指定等待的对象句柄个数。
l 参数lpHandles为指向等待对象数组的指针,注意数组可以包含不同类型的对象句柄。
l 参数fWaitAll指定等待类型。在Windows CE中这个参数必须被设置为FALSE,表示只要等待的句柄数组中指向的任一个对象被设置为信号态,等待就返回。
l 参数dwMilliseconds指定超时时间,与WaitForSingleObject()中的超时时间用法相同。
当调用成功,WaitForMultipleObjects()函数的返回值指示导致函数返回的事件标识以及返回的原因,可取的返回值有:
l WAIT_OBJECT_0 to (WAIT_OBJECT_0 + nCount –1):表示至少一个等待的对象被设置为信号态。进一步将返回值减去WAIT_OBJECT_0就可以得到被设置为信号态的对象句柄在数组中的下标;如果多个对象为信号态,则返回最小的下标。
l WAIT_TIMEOUT:表示等待超时而没有一个对象为信号态。
l WAIT_FAILED: 表示函数调用失败,通过调用GetLastError()函数可以得到出错信息。
应用程序还可以将数据关联到事件句柄上,通过SetEventData()函数实现:
BOOL SetEventData(
HANDLE hEvent,
DWORD dwData
);
l 参数hEvent指定将被关联数据的事件对象的句柄。
l 参数dwData指定关联的数据,可以为指向某个内存位置的指针
通过GetEventData()函数可以获取事件句柄关联的数据:
DWORD GetEventData(
HANDLE hEvent
);
这个函数返回之前关联到事件句柄的数据,如果返回0,表示调用失败。
下面的代码演示如何在线程中等待
int EventsExample (void)
{
HANDLE hEvents[2];
DWORD dwEvent;
int i;
for (i = 0; i < 2; i++)
{
hEvents[i] = CreateEvent (NULL, // 不支持事件安全属性,必须为NULL
FALSE, // 创建系统重置事件
FALSE, // 初始化为非信号态
NULL); // 非命名事件
if (hEvents[i] == NULL)
{
// 创建事件对象失败
MessageBox (NULL, TEXT("CreateEvent error!"),
TEXT("Error"), MB_OK);
// 可以在这里调用GetLastError()函数获取错误信息
return 0;
}
}
// 在这里新创建线程,并在线程内将事件对象设置为信号态
//等待事件对象
dwEvent = WaitForMultipleObjects (
2, // 数组中事件句柄的个数
hEvents, // 事件句柄数组
FALSE, // 必须为FALSE,只能等待任意事件句柄
INFINITE); // 无限等待
switch (dwEvent)
{
case WAIT_OBJECT_0 + 0: // 第一个事件对象变为信号态
case WAIT_OBJECT_0 + 1: // 第二个事件对象变为信号态
break;
default:
// 等待事件函数出错
MessageBox (NULL, TEXT("Wait error!"),
TEXT("Error"), MB_OK);
return 0;
}
return 1;
}
6.3.2 线程等待
在前面等待事件对象的章节中我们已经介绍了两种等待函数:WaitForSingleObject()和WaitForMultipleObjects()。而对于需要负责处理消息循环的线程(通常为程序的主线程),应该使用MsgWaitForMultipleObjects()或MsgWaitForMultipleObjectsEx()函数,这是因为当线程被前面两个等待函数阻塞时无法获取和分发消息循环中的消息。另外在Windows CE中,多个对象的等待函数不用等到对象数组中的所有对象都被设置为信号态后才能返回继续执行,实际上只需要等待其中的任意一个对象进入信号态就可以返回,这点需要注意。使用线程等待的好处是,当线程被阻塞时,线程并不是一直占用CPU(通常称为忙等待),而是进入一个十分高效的节能状态,只消耗很少的能耗。
MsgWaitForMultipleObjects()函数能够在等待多个对象的同时,也等待消息,通过将指定类别的消息加入到对象集合中实现,这样当接收到指定类别的消息后,等待函数也会返回。这个函数的原型如下:
DWORD MsgWaitForMultipleObjects(
DWORD nCount,
LPHANDLE pHandles,
BOOL fWaitAll,
DWORD dwMilliseconds,
DWORD dwWakeMask
);
前面4个参数的含义和用法与前面介绍过WaitForMultipleObjects()中的参数一样。最后一个参数dwWakeMask指定将被加到等待对象集合中的消息类别,可取值有:
l QS_ALLEVENTS:队列中的一个输入,WM_TIMER, WM_PAINT, WM_HOTKEY, 或者其它被投递的消息。
l QS_ALLINPUT:队列中的任何消息。
l QS_HOTKEY:队列中的WM_HOTKEY消息。
l QS_INPUT: 队列中的一个输入消息。
l QS_KEY: 队列中的按键消息,包括WM_KEYUP(抬起), WM_KEYDOWN(按下), WM_SYSKEYUP(系统按键抬起)以及WM_SYSKEYDOWN(系统按键按下)。
l QS_MOUSE: 一个鼠标移动(WM_MOUSEMOVE)或鼠标键(WM_LBUTTONUP, WM_RBUTTONDOWN等)消息。
l QS_MOUSEBUTTON: 一个鼠标键消息。
l QS_MOUSEMOVE:一个队列中的鼠标移动消息。
l QS_PAINT:一个队列中的WM_PAINT消息。
l QS_POSTMESSAGE:接收到这个列表以外的被投递的消息。
l QS_SENDMESSAGE:在队列中由其它线程或应用程序发送的消息。
l QS_TIMER:队列中的WM_TIMER消息。
这个函数的返回值指示等待函数返回的原因,可取的返回值有:
l WAIT_OBJECT_0 到 (WAIT_OBJECT_0 + nCount –1):如果返回值落在这个范围,表示至少一个对象变为信号态,将这个返回值减去WAIT_OBJECT_0可以得到对象在数组中的下标。
l WAIT_OBJECT_0 + nCount: 由dwWakeMask指定类型的新消息进入线程的输入队列
l WAIT_ABANDONED_0 to (WAIT_ABANDONED_0 + nCount–1) : 表示等待对象数组中的某个对象被终止。
l WAIT_TIMEOUT: 等待超时,但没有对象变为信号态,而且没有指定类型的新消息进入队列。
l 0xFFFFFFFF:表示函数执行失败。
MsgWaitForMultipleObjectsEx()函数实现的功能与MsgWaitForMultipleObjects()基本相同,不过MsgWaitForMultipleObjectsEx()函数可以进一步指定如果消息队列中已经有消息,函数是否能够立即返回,原型为:
DWORD MsgWaitForMultipleObjectsEx(
DWORD nCount,
LPHANDLE pHandles,
DWORD dwMilliseconds,
DWORD dwWakeMask,
DWORD dwFlags
);
这个函数已经舍弃了fWaitAll参数,前面4个参数的涵义与MsgWaitForMultipleObjects()函数一样。最后一个参数dwFlags指定等待类型,可以设置为以下两个值或它们的组合:
l 0:表示当任意一个对象变为信号态,函数返回。
l MWMO_INPUTAVAILABLE: 表示在函数调用时,如果队列中已经有消息,函数立即返回。
实际上MsgWaitForMultipleObjects()函数的功能与将这里的dwFlags设置为0是相同的。
前面介绍过这几个等待函数除了可以等待事件外,还可以等待进程和线程的句柄。实际上可以把进程或线程句柄看作特殊的同步对象:当它们在执行中时,一直处于非信号态;当它们被终止后,就进入信号态。这在有些时候特别有用,比如一个进程创建了一个子进程,然后调用等待函数阻塞,知道子进程终止,才能恢复执行,这样就能防止父进程先于子进程结束并被终止。
6.3 3 信号量
信号量也是内核同步对象,比事件对象稍复杂。事件对象只有两种状态信号态或非信号态,无法对事件的状态通过计数设置。而信号量则在内部维护一个计数,只有当计数值大于0,信号量才处于信号态,当计数值为0,信号量处于非信号态。信号量也分为命名信号量和非命名(匿名)信号量,其中命名信号量可被用于在不同进程的线程间实现同步。
信号量常被用于保护一个资源在同一个时间只能被一定数量的线程访问,当一个等待线程每次因为信号量变为信号态而从等待中返回时,信号量的计数值减1;当一个线程完成操作并释放信号量时,信号量的计数值加1。在创建信号量对象的时候需要指定信号量的最大计数值,比如创建一个具有最大计数值为4的信号量来保护一个读写缓冲区,这就表示最多同时允许4个线程(比如读线程)获取缓冲区,前4个线程调用等待函数都不会阻塞,当第5个线程请求等待时,将导致阻塞,因为此时信号量的计数值已经减为0,直到前面某一个线程释放信号量。
与事件对象一样,线程等待还是使用上面介绍过的WaitForSingleObject()或WaitForMultipleObjects()函数。
在使用信号量之前,受限需要创建一个信号量,函数CreateSemaphore()用于创建一个信号量对象:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
l 参数lpSemaphoreAttributes设置信号量对象的安全属性,在Windows CE中不支持,必须设为NULL。
l 参数lInitialCount指定信号量对象的初始计数值,这个值必须大于等于0,小于等于最大计数值。
l 参数lMaximumCount指定信号量对象的最大计数值
l 参数lpName信号量对象的名字,如果需要创建全局命名信号量对象。这个参数可以设置为NULL,表示创建非命名信号量对象。命名信号量可以在不同进程的线程之间实现同步,它们实际都指向同一个信号量对象。如果传递进来的名字已经存在,那么此时将打开已经创建的信号量的句柄,参数lInitialCount和参数lMaximumCount将被忽略。
这里设置的初始值决定了信号量的状态,如果信号量进入非信号状态,需要调用ReleaseSemaphore()函数释放信号量,增加信号量的计数值(可以大于1):
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
l 参数hSemaphore指定信号量句柄
l 参数lReleaseCount指定本次释放操作增加的信号量计数的值,该值必须大于0。尽管在大多数情况下,这个值都设为1,但是有些时候还是需要将计数值增加不止1,比如下面将介绍到的应用程序初始化过程。如果增加的计数值导致超过信号量的最大计数值,将导致释放信号量失败,计数值不会变化,函数将放回FALSE。
l 参数lpPreviousCount为输出参数,用于返回信号量上一次的计数值,如果不需要,可以将这个参数设置为NULL。
ReleaseSemaphore()函数还经常被用在应用程序的初始化操作。首先应用程序创建一个初始计数值为0的信号量,这样导致信号量初始化为非信号状态,将导致所有请求访问保护资源的线程都被阻塞。等到应用程序初始化完成,应用程序再调用ReleaseSemaphore()函数将信号量的计数值增加到最大值,这样保证线程不会访问那些未被正确初始化的保护资源。
再次强调,在Windows CE环境下,内存资源比较稀缺,一旦句柄使用完毕,就应该及时关闭,对于信号量也是这样。调用CloseHandle()函数关闭信号量句柄,导致它的引用计数减1,只有当引用计数减为0,内核才会释放信号量对象。所以对于全局命名信号量,各线程的CloseHandle()函数的调用次数必须和CreateSemaphore()函数相同。
6.3.4 互斥量
互斥量是另一种内核同步对象,同样具有信号状态和非信号状态。可以把互斥量看作是最大计数值为1的信号量,每次最多只允许一个线程访问互斥量保护的资源。当没有被线程占用时,互斥量处于信号状态;一旦被某一个线程占用,互斥量就变为非信号态。互斥量可以被创建为命名或非命名,线程通过调用等待函数请求获取互斥量的所有权。如果互斥量处于信号状态,那么线程将获取互斥量的所有权,此时互斥量就进入非信号状态,之后其它线程的请求都将导致阻塞,直到占用互斥量的线程显式释放信号量。
函数CreateMutex()用于创建互斥量,原型为:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
l 参数lpMutexAttributes设置互斥量的安全属性,在Windows CE中不支持,设置为NULL。
l 参数bInitialOwner指定创建线程是否占用互斥量。如果值为TRUE,表示线程在互斥量创建后直接获取所有权,则互斥量被初始化为非信号态,否则不获取所有权,初始化为信号态。
l 参数lpName指定命名互斥量的名字,如果希望创建命名互斥量。否则设为NULL,表示创建非命名互斥量。命名互斥量可用于在不同进程的线程之间实现同步,如果指定名字的互斥量已经存在,将打开已经存在的互斥量句柄,而不会再创建,此时参数bInitialOwner 将被忽略,GetLastError()函数将返回ERROR_ALREADY_EXISTS。
一种情况是用户希望创建线程立即获取互斥量所有权(将参数bInitialOwner设为TRUE),但是指定名字的信号量已经存在,此时是无法保证线程能够获取所有权的,而且也不会导致线程阻塞,用户则会错误地认为已经获取所有权,导致保护失败。这种情况下,在调用CreateMutex()函数之后,通过GetLastError()函数查询互斥量是否已经被创建是必要的,如果已经被创建,必须调用等待函数来请求获取所有权。
互斥量使用完毕,需要获得所有权的线程显式释放,通过ReleaseMutex()函数实现:
BOOL ReleaseMutex(
HANDLE hMutex
);
l 参数hMutex指定互斥量句柄。
当调用线程并没有获取互斥量的所有权时,调用将失败,返回0。
一旦线程已经获取了互斥量对象的所有权,它可以再次调用等待函数请求这个对象的所有权而不会被阻塞。这样能够防止线程因为等待自己已经所有的互斥量对象受阻塞而引起的死锁,实际上互斥量对象内部维护了等待函数调用次数的计数,占用线程每次调用了多少次等待函数,也需要调用相同次数的ReleaseMutex()才能释放互斥量对象。下面的代码演示该如何创建和使用互斥量对象:
void NamedMutexExample (void)
{
HANDLE hMutex;
TCHAR szMsg[100];
//创建命名互斥量对象
hMutex = CreateMutex (
NULL, // 不支持安全属性
FALSE, // 初始化为不立即所有互斥量对象
TEXT("NameOfMutexObject")); // 命名互斥量的名字
if (NULL == hMutex)
{
// 创建互斥量对象失败
// 获取出错信息
wsprintf (szMsg, TEXT("CreateMutex error: %d."), GetLastError ());
MessageBox (NULL, szMsg, TEXT("Error"), MB_OK);
}
else
{
// 函数调用成功,需要进一步判断命名互斥量是否已经存在
if ( ERROR_ALREADY_EXISTS == GetLastError () )
MessageBox (NULL, TEXT("CreateMutex opened existing mutex."),
TEXT("Results"), MB_OK);
else
MessageBox (NULL, TEXT("CreateMutex created new mutex."),
TEXT("Results"), MB_OK);
}
}
6.3.5 互锁函数
如果我们只是需要对变量进行简单的增加,减少或变量间互换,那么应该考虑使用互锁函数来保证访问同步。互斥函数是一种比较底层的同步方法,提供了用于同步对多个线程共享变量访问的简单机制。互锁函数能够防止一个线程在增加或检查一个变量时被抢占。如果变量在共享内存中,互锁函数可以在不同进程的线程之间使用。互锁函数的实现更加简单,比互斥量,信号量等同步机制更快更有效。Windows CE7共支持8种不同的互锁函数,下面分别介绍:
LONG InterlockedIncrement(
LPLONG lpAddend
);
LONG InterlockedDecrement(
LPLONG lpAddend
);
LONG InterlockedExchange(
LPLONG Target,
LONG Value
);
PVOID InterlockedExchangePointer(
PVOID* Target,
PVOID Value
);
其中InterlockedIncrement()用于将指定的32位变量加1,并返回加1后的结果;InterlockedDecrement()函数则将指定的32位变量减1并返回结果;而InterlockedExchange()函数用于将指定的值Value设置到目标变量Target中,并返回目标变量的原值。InterlockedExchangePointer()函数实现的功能和InterlockedExchange()函数一样,但是可以针对其它类型的数据(的指针)。
LONG InterlockedCompareExchange(
LPLONG Destination,
LONG Exchange,
LONG Comperand
);
PVOID InterlockedCompareExchangePointer(
PVOID* Destination,
PVOID ExChange,
PVOID Comperand
);
InterlockedCompareExchange()函数稍微复杂一点,首先对目标变量Destination和参数Comperand的值执行原子比较,如果它们的值相等,那么就将Exchange参数的值设置到目标变量中;否则不会执行设置目标变量的操作。这个函数总是返回目标变量的原值。InterlockedCompareExchangePointer()函数实现的功能和InterlockedCompareExchange()函数一样,可以针对其他类型的数据指针进行操作。这两个函数都会忽略参数的符号位,这点需注意。
LONG WINAPI InterlockedTestExchange(
LPLONG Target,
LONG OldValue,
LONG NewValue
);
InterlockedTestExchange()函数用于执行条件原子设置指令,如果参数Target指向的目标变量的值和参数OldValue的值相等,就将参数NewValue的值原子设置到目标变量中,否则不会执行设置操作。这个函数总是返回目标变量的原值,所以如果函数返回值与参数OldValue的值相等,就表示成功地将NewValue的值设置到目标变量中。
LONG InterlockedExchangeAdd(
LPLONG Addend,
LONG Increment
);
InterlockedExchangeAdd()函数将Increment参数的值原子加到Addend指向的变量中,而不是简单地加1,这个函数返回Addend指向的变量的原值。
6.3.6 临界区
临界区是实现线程间同步的另一种有效方法,它能够防止多个获取访问权限的线程在同一时间内访问同一个代码区域,它们可能导致数据的不一致。临界区对象是系统内部最基本的互斥方式,一般被用于保证一段代码执行的原子性。临界区与互斥量相似,但是临界区对象的使用被限制在同一个进程的上下文中,不能在不同进程的线程之间实现同步,而互斥量可以在不同的进程之间共享(这里指命名互斥量)。临界区的优势是它的速度比互斥量快,如果不需要在进程间共享资源,应该优先考虑使用临界区。临界区的使用比较直观,线程在进入临界区之前,通过调用EnterCriticalSection()或TryEnterCriticalSection()函数来申请对临界区的占用权限,如果之后同一个进程内的其它线程也调用EnterCriticalSection()函数,申请同一个临界区对象的占用权限,将导致另一线程被阻塞,直到前面线程调用LeaveCriticalSection()函数释放对临界区的占用。
同样在使用临界区对象之前需要先向系统申请一个临界区对象,函数InitializeCriticalSection()实现这个功能:
void InitializeCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
l 参数lpCriticalSection为指向临界区对象的指针,这是一个CRITICAL_SECTION结构,进程需要负责为临界区对象分配内存空间,注意不要将临界区对象分配在函数的栈上,因为它会在函数返回后将被释放,而应该将临界区对象定义在所有对其操作函数可见的范围内(比如放到堆上)。临界区对象不能被移动或复制,进程也不能直接操作临界区对象,而必须通过Win API提供的接口函数来管理临界区对象。在实际使用时,将指向CRITICAL_SECTION结构的指针当作句柄来使用。
在正确初始化临界区对象后,就可以申请获取临界区的占用权限,应调用下面两个函数之一:
void EnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
BOOL TryEnterCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
这两个函数都只有一个参数lpCriticalSection,为指向临界区结构的指针。这两个函数的区别是,如果此时临界区已经被其它线程占用,EnterCriticalSection()函数将导致调用线程阻塞,直到临界区被其它线程释放为止;而TryEnterCriticalSection()函数不会导致调用线程阻塞,马上返回,如果返回值为非0,表示获取临界区占用权成功,返回0则表示临界区已经被其它线程占用。如果一个线程已经获取对临界区的占用权限,再次调用EnterCriticalSection()不会导致线程阻塞,这样防止线程因为等待自己已经占用的临界区而导致死锁。
退出临界代码区域前,需要先调用LeaveCriticalSection()函数释放对临界区对象的占有:
void LeaveCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
参数lpCriticalSection为指向临界区对象的指针。如果一个并没有获得临界区对象占用权限的线程调用LeaveCriticalSection()函数导致出错而可能使其它线程的请求函数的等待变得不确定。
在临界区对象使用完毕之后,需要将临界区的资源释放掉,调用DeleteCriticalSection()函数实现:
void DeleteCriticalSection(
LPCRITICAL_SECTION lpCriticalSection
);
参数lpCriticalSection为指向临界区对象的指针。在临界区的资源被释放掉之后,这个临界区对象就不能再被使用。而且要确保临界区不被任何线程占用之后,才能释放临界区资源,否则将导致那些等待临界区的线程状态变得不确定。下面的例子演示如何初始化、进入、离开临界区,并使用临界区对共享变量进行保护:
CRITICAL_SECTION csMyCriticalSection;
InitializeCriticalSection (&csMyCriticalSection);
__try
{
EnterCriticalSection (&csMyCriticalSection);
// 获得临界区占用权限,在这里对受保护资源进行操作
}
__finally
{
// 离开临界区,释放对临界区的占用
LeaveCriticalSection (&csMyCriticalSection);
}
…
//记得最后释放临界区对象资源
DeleteCriticalSection(&csMyCriticalSection);
6.3.7 一个线程间同步的例子
下面实现一个利用互斥量实现线程间同步访问的例子。这个例子实现一个简单的多个线程争取一定数量的票,票数一共为1,000,000。票是全局的,需要一个互斥锁来保护,每个线程获取票之前需要先申请获取互斥锁,然后才能取票。这个程序的运行结果如图6.3所示。
图6.3 多个线程同步的运行结果
左边3个文本框为只读,输出每个线程最终获取的票数。右边的”Start”按钮用于模拟开始取票,这里程序会创建3个线程,然后每个线程分别取票。
对话框类的头文件如下:
// SynchronizeThreadMutexDlg.h : header file
//
#pragma once
#include "afxwin.h"
// CSynchronizeThreadMutexDlg dialog
class CSynchronizeThreadMutexDlg : public CDialog
{
// Construction
public:
CSynchronizeThreadMutexDlg(CWnd* pParent = NULL); // standard constructor
// Dialog Data
enum { IDD = IDD_SYNCHRONIZETHREADMUTEX_DIALOG };
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
virtual BOOL OnInitDialog();
#if defined(_DEVICE_RESOLUTION_AWARE) && !defined(WIN32_PLATFORM_WFSP)
afx_msg void OnSize(UINT /*nType*/, int /*cx*/, int /*cy*/);
#endif
DECLARE_MESSAGE_MAP()
public:
CButton m_ButtonStart;
CEdit m_EditThread1;
CEdit m_EditThread2;
CEdit m_EditThread3;
afx_msg void OnBnClickedButtonStart();
static DWORD ThreadProc(PVOID pArg);
protected:
};
这里还是将线程函数ThreadProc实现为对话框类的静态函数:
public:
static DWORD ThreadProc(PVOID pArg);
在对话框类的实现文件的开始定义几个全局变量:g_Tickets为总的票数,这里初始设置为1,000,000;g_HoldTicketsNum用于存放每个线程各自获取的票数;g_hMutex为一个互斥量,用于保护全局的票;下面的m_threadID用于存放线程的标识,通过这个标识,每个线程函数才能正确更新自己的票数;m_hThread则是每个线程的句柄。
//全局变量
unsigned int g_Tickets = 1000000; //总的Ticket个数
unsigned int g_HoldTicketsNum[3]; // 用于存放每个线程获取的ticket个数
HANDLE g_hMutex; //使用Mutex保护全局的Ticket
int m_threadID[3];
HANDLE m_hThread[3];
下面是对话框类的初始化函数,这里调用CreateMutex()函数创建互斥量,并初始化为信号态:
BOOL CSynchronizeThreadMutexDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Set the icon for this dialog. The framework does this automatically
// when the application's main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// TODO: Add extra initialization here
m_EditThread1.SetWindowTextW(_T("0"));
m_EditThread2.SetWindowTextW(_T("0"));
m_EditThread3.SetWindowTextW(_T("0"));
int i;
for(i = 0; i < 3; i++)
{
m_threadID[i] = i;
g_HoldTicketsNum[i] = 0;
}
//创建Mutex对象
g_hMutex = CreateMutex(
NULL, //不支持安全属性
0, //初始化为信号态
NULL); //创建匿名互斥量
return TRUE; // return TRUE unless you set the focus to a control
}
线程函数ThreadProc()的实现比较简单,首先判断票数是否大于0,如果是就申请获取互斥锁,获得锁以后,就将全局的票数减一,并增加自己获取的票数。线程函数通过传递进来的参数pArg来得到自己的标识:
DWORD CSynchronizeThreadMutexDlg::ThreadProc(PVOID pArg)
{
//获取线程标识,用于更新指定下标的计数
int tid = *((int*)pArg);
while(g_Tickets > 0){
//等待互斥量
WaitForSingleObject(g_hMutex,INFINITE);
g_Tickets--;
//更新进程获取的ticket个数
g_HoldTicketsNum[tid]++;
//释放对互斥量的占有
ReleaseMutex(g_hMutex);
}
return 0;
}
最后就是”Start”按钮的单击事件处理函数,首先初始化每个线程获取的票数为0,将总票数设置回初始值1,000,000,因为这几个是全局变量,所以每次都需要重新设置,表示进行新一轮的取票过程。然后就创建3个线程,分别启动它们之后,主线程就调用WaitForSingleObject()函数分别等待每个线程的结束。统计好每个线程的票数并输出到对应文本框中,最后需要关闭线程句柄。
void CSynchronizeThreadMutexDlg::OnBnClickedButtonStart()
{
// TODO: Add your control notification handler code here
int i;
for(i = 0; i < 3; i++)
{
g_HoldTicketsNum[i] = 0;
}
g_Tickets = 1000000;
m_ButtonStart.EnableWindow(false);
for( i = 0; i < 3; i++ )
{
m_hThread[i] = CreateThread(
NULL, //不支持安全属性
0, //线程使用默认栈大小
ThreadProc, //线程入口函数
&m_threadID[i], //传递给线程的参数,这里指定为操作的下标
CREATE_SUSPENDED, //挂起线程
NULL); //不需要获取线程标识
if(!m_hThread[i])
{
CString failedMsg;
failedMsg.Format(_T("Thread %d create failed"),i);
AfxMessageBox(failedMsg);
}
}
//同时启动线程
for( i = 0; i < 3; i++ )
{
ResumeThread(m_hThread[i]);
}
//等待每个线程结束
for( i = 0; i < 3; i++ )
{
WaitForSingleObject(m_hThread[i],INFINITE);
}
//Sleep(5000);
//
//WaitForMultipleObjects(3,m_hThread,TRUE,INFINITE);
AfxMessageBox(_T("Get tickets finished!"));
//显示每个线程获取的ticket个数
CString holdTickets;
holdTickets.Format(_T("%u"),g_HoldTicketsNum[0]);
//AfxMessageBox(holdTickets);
m_EditThread1.SetWindowTextW(holdTickets);
holdTickets.Format(_T("%u"),g_HoldTicketsNum[1]);
m_EditThread2.SetWindowTextW(holdTickets);
holdTickets.Format(_T("%u"),g_HoldTicketsNum[2]);
m_EditThread3.SetWindowTextW(holdTickets);
//关闭线程句柄
for( i = 0; i < 3; i++ )
{
CloseHandle(m_hThread[i]);
}
m_ButtonStart.EnableWindow(true);
}
一点值得注意的是,等待线程结束使用函数WaitForSingleObject()实现,而且必须分别等待每个线程,但是不能对多个线程调用WaitForMultipleObjects()来实现等待多个线程,Windows CE7无法实现这样的等待:
//等待每个线程结束
for( i = 0; i < 3; i++ )
{
WaitForSingleObject(m_hThread[i],INFINITE);
}
//Sleep(5000);
//同时等待多个线程无法实现
//WaitForMultipleObjects(3,m_hThread,TRUE,INFINITE);
6.4 小结
本章主要介绍在Windows CE7系统中的多进程,多线程的基本概念和编程实现。首先介绍进程如何创建,终止以及其它的进程操作函数。然后介绍线程的概念以及在Windows CE7种采用的线程优先级,以及线程调度。接着介绍线程操作的各种API,包括创建线程,设置和查询线程优先级,设置和查询线程时间片,挂起和恢复线程。最后介绍线程的同步机制以及它们的操作API,包括事件,线程等待,信号量,互斥量,互锁函数和临界区