我 们已经讨论了好几种内核对象,包括进程、线程以及作业。几乎所有这些内核对象都可以用来进行同步。对线程同步来说,这些内核对象中的每一种要么处于触发状 态,要么处于未触发状态。Microsooft为每种对象创建了一些规则,规定如何在这两种状态之间进行转换。用内核对象进行线程同步就是要用这些内核对 象的规则为线程同步服务。
2.等待函数
等待函数 使一个线程自愿进入等待状态,直到指定的内核对象被触发为止。注意,如果线程在调用一个等待函数的时候,相应的内核对象已经处于触发状态,那么线程时不会进入等待状态的。
DWORD WaitForSingleObject(
HANDLE hObject, //标识要等待的内核对象,这个内核对象可以处于触发或未触发状态;
DWORD dwMilliseconds); //指定线程最多愿意花多长的时间来等待对象被触发;
通常,我们会给dwMilliseconds传入INFINITE,但也可以传任何其它的值(以微妙微单位)。传INFINITE可能会有点危险。如果对象永远不被触发,那么调用线程将永远不会被唤醒——它会一直阻塞在那里,但幸运的是,它并没有浪费宝贵的CPU时间。
DWORD dw = WaitForSingleObject(hProcess, 5000);
switch (dw) {
case WAIT_OBJECT_0:
// 等待对象被触发,即等待进程已终止;
break;
case WAIT_TIMEOUT:
// 等待超时,即等待的进程在5000微妙内未终止;
break;
case WAIT_FAILED:
// 无效的参数,如无效的进程句柄
break;
}
前述代码告诉系统,除非指定的进程已经终止或者等待时间已满5000微妙,否则不应该对调用线程进行调度。如果进程已经终止,那么这个调用会在5000微妙内返回,如果进程尚未终止,那么这个调用大约会在5000微妙左右返回。注意,如果给dwMilliseconds传0,WaitForSingleObject总会立即返回,即使它要等待的条件还没有满足。
还有一个函数允许调用线程同时检查多个内核对象的触发状态:
DWORD WaitForMultipleObjects(
DWORD dwCount,//表示希望检查的内核对象的数量;最大值MAXIMUM_WAIT_OBJECTS=64
CONST HANDLE* phObjects,//指向内核对象句柄数组
BOOL bWaitAll,//TRUE,等待所有内核对象触发;FALSE,等待其中任一对象触发;
DWORD dwMilliseconds); //指定线程最多愿意花多长的时间来等待对象被触发;
HANDLE3];
h[0] = hProcess1;
h[1] = hProcess2;
h[2] = hProcess3;
DWORD dw = WaitForMultipleObjects(3, h, FALSE, 5000);
switch (dw) {
case WAIT_FAILED:
// Bad call to function (invalid handle?)
break;
case WAIT_TIMEOUT:
// None of the objects became signaled within 5000 milliseconds.
break;
//指定具体哪个进程触发引起返回的
case WAIT_OBJECT_0 + 0:
// The process identified by h[0] (hProcess1) terminated.
break;
case WAIT_OBJECT_0 + 1:
// The process identified by h[1] (hProcess2) terminated.
break;
case WAIT_OBJECT_0 + 2:
// The process identified by h[2] (hProcess3) terminated.
break;
}
3.等待成功所引起的副作用
我们在调用WaitForSingleObject或WaitForMultipleObjects有的时候可能 会改变对象的状态。如果对象的状态发生了改变,我们称之为等待成功所引起的副作用。 举 个例子,现在假设线程正在等待一个自动重置事件对象(稍后会讲到)。当事件对象被触发的时候,函数会检测到这一情况,这时它可以直接返回 WAIT_OBJECT_0给调用线程。但是,就在函数返回之前,它会使事件变为非触发状态——这就是等待成功所引起的副作用。然而进程和线程对象就完全 没有副作用,也就是说,等待这些对象绝对不会改变对象的状态。
4.事件内核对象
事件的触发表示一个操作已经完成。有两种不同类型的事件对象:手动重置事件 和自动重置事件。 当一个手动重置事件被触发的时候,正在等待该事件的所有线程都将变为可调度状态。而当自动重置事件被触发的时候,只有一个正在等待该事件的线程会变成可调度状态。
事件最通常的用途是,让一个线程执行初始化工作,然后再触发另一个线程,让它执行剩余的工作。以开始我么么你 将事件初始化为为触发状态,然后当线程完成初始化工作的时候,触发事件。此时,另一个线程一直在等待该事件,它发现事件被触发,于是鞭策很能够可调度状 态。第二个线程知道第一个线程已经完成了它的工作。
下面是CreateEvent函数,用来创建一个事件内核对象:
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,//TRUE,创建手动重置事件;FALSE,创建自动重置事件。
BOOL bInitialState,//TRUE,初始化为触发状态;FALSE,初始化为未触发状态。
PCTSTR pszName);
一旦创建了事件,我们就可以直接控制它的状态。当调用SetEvent的时候,我们把时间变成触发状态:
BOOL SetEvent(HANDLE hEvent);
当调用ResetEvent的时候,我们把时间变成未触发状态:
BOOL ResetEvent(HANDLE hEvent);
Microsoft为自动重置事件 定义了一个等待成功所引起 的副作用:当线程成功等到自动重置事件对象的时候,对象会自动地重置为未触发状态。这也是自动重置事件名字的由来。对自动重置事件来说,通常不需要调用 ResetEvent,这是因为系统会自动将事件重置。相反,Microsoft并没为手动重置事件对象定义一个等待成功所引起的副作用。
5.可等待的计时器内核对象
可等待的计时器是这样一种内核对象,它们会在某个指定的事件触发,或没隔一段时间触发一次。它们通常用来在某个时间执行一些操作。
创建可等待的计时器:
HANDLE CreateWaitableTimer(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,//TRUE,手动重置计时器,FALSE,自动重置计时器;
PCTSTR pszName);
当手动重置计时器被触发的时候,正在等待该计时器的所有线程都会变成可调度状态。当自动重置计时器被触发的时候,只有一个正在等待该计时器大的线程变成可调度状态。
在创建的时候,可等待的计时器对象总是处于未触发状态。调用一下函数来处罚计时器:
BOOL SetWaitableTimer(
HANDLE hTimer,
const LARGE_INTEGER *pDueTime,//计时器第一次触发的时间;
LONG lPeriod,//第一次触发之后,计时器应该以怎样的频度触发;
PTIMERAPCROUTINE pfnCompletionRoutine,//APC调用相关
PVOID pvArgToCompletionRoutine,
BOOL bResume);
取消计时器,这样计时器就永远不会触发了,除非再调用SetWaitableTimer来对它进行重置:
BOOL CancelWaitableTimer(HANDLE hTimer);
可等待计时器和用户计时器(通过SetTimer函数来设置)的最大区别在于用户计时器需要在应用程序中使用大量的用户界面基础设施,从而消耗更多的资源。此外可等待计时器是内核对象,这意味着它们不仅可以在多个线程间共享,而且可以具备安全性。
6.信号量内核对象
信号量内核对象用来对资源进行计数。与其它所有内核对象相同,它们也包含一个使用计数,但他们还包含另外两个32位值:一个最大资源计数和一个当前资源计数。
信号量的规则如下:
创建信号量内核对象:
HANDLE CreateSemaphore(
PSECURITY_ATTRIBUTE psa,
LONG lInitialCount,//资源初始计数
LONG lMaximumCount,//资源的最大计数
PCTSTR pszName);
如果等待函数发现信号量当前资源计数为0(信号量处于未触发状态),那么系统会让调用线程进入等待状态。当另一个线程将信号量的当前资源计数递增时,系统会记得那个还在等待的线程,使它们变成可调度状态(并相应递减当前资源计数)。
线程通过调用ReleaseSemaphore来递增心好像的当前资源计数:
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,//把这个值加到资源计数上,通常为1
PLONG plPreviousCount);//返回当前资源计数的原始值;
7.互斥量内核对象
互斥量内核对象用来确保一个线程独占对一个资源的访问。互斥量与关键段的行为完全相同。但是,互斥量是内核对象,而关键段是用户模式下的同步对象。
互斥量的规则:
创建一个互斥量:
HANDLE CreateMutex(
PSECURITY_ATTRIBUTES psa,
BOOL bInitialOwner,//指定是否为调用线程所有
PCTSTR pszName);
当目前占用访问权的线程不再需要访问资源的时候,它必须调用ReleaseMutex函数来释放互斥量:
BOOL ReleaseMutex(HANDLE hMutex);
特征 |
互斥量 |
关键段 |
---|---|---|
性能 |
慢 |
块 |
是否能跨进程使用 |
是 |
否 |
声明 |
HANDLE hmtx; |
CRITICAL_SECTION cs; |
初始化 |
hmtx = CreateMutex (NULL, FALSE, NULL); |
InitializeCriticalSection(&cs); |
清理 |
CloseHandle(hmtx); |
DeleteCriticalSection(&cs); |
无限等待 |
WaitForSingleObject (hmtx, INFINITE); |
EnterCriticalSection(&cs); |
0等待 |
WaitForSingleObject (hmtx, 0); |
TryEnterCriticalSection(&cs); |
任意时间长度的等待 |
WaitForSingleObject (hmtx, dwMilliseconds); |
不支持 |
释放 |
ReleaseMutex(hmtx); |
LeaveCriticalSection(&cs); |
是否能同时等待其它 内核对象 |
是 (使用WaitForMultipleObjects 或类似函数) |
否 |
表1:互斥量和关键段比较
8.线程同步对象速查表
对象 |
何时处于未触发状态 |
何时处于触发状态 |
成功等待的副作用 |
进程 |
当进程仍在运行的时候 |
当进程终止运行时(ExitProcess, Te rminateProcess) |
无 |
线程 |
当线程仍在运行时 |
当线程终止运行时(ExitThread, TerminateThread) |
无 |
作业 |
当作业尚未超时的时候 |
当作业超时的时候 |
无 |
文件 |
当I / O请求正在处理时 |
当I / O请求处理完毕时 |
无 |
控制台输入 |
不存在任何输入 |
当存在输入时 |
无 |
文件修改通知 |
没有任何文件被修改 |
当文件系统发现修改时 |
重置通知 |
自动重置事件 |
ResetEvent , PulseEvent或等待成功 |
当调用SetEvent / PulseEvent时 |
重置事件 |
手动重置事件 |
ResetEvent或PulseEvent |
当调用SetEvent / PulseEvent时 |
无 |
自动重置等待计时器 |
CancelWaitableTimer或等待成功 |
当时间到时(SetWaitableTimer) |
重置定时器 |
手动重置等待计时器 |
CancelWaitableTimer |
当时间到时(SetWaitableTimer) |
无 |
信号量 |
等待成功 |
当数量> 0时(ReleaseSemaphore) |
数量递减1 |
互斥对象 |
等待成功 |
当未被线程拥有时(Release互斥对象) |
将所有权赋予线程 |
关键代码段(用户模式) |
等待成功((Try)EnterCriticalSection) |
当未被线程拥有时(LeaveCriticalSection) |
将所有权赋予线程 |
SRWLock (用户模式) |
等待成功的时候 (AcquireSRWLock(Exclusive)) |
不为线程占用的时候 (ReleaseSRWLock(Exclusive)) |
把所有权交给线程 |
条件变量 (用户模式) |
等待成功地时候 (SleepConditionVariable*) |
被唤醒的时候 (Wake(All)ConditionVariable) |
没有 |