事件(Event)对象可以说是最简单的内核对象,它包含一个引用计数、一个用来表示该对象是自动重置还是手动重置的布尔值和一个表示对象当前状态(signaled或unsignaled)的布尔值。事件标志着某动作的完成,事件对象分为两类:manual-reset(手动重置事件)和auto-reset(自动重置)事件。当manual-reset事件状态变为signaled时,所有等待该事件的线程都成为可调度的,当auto-reset事件状态变为signaled时,等待该事件的所有线程中只会有一个成为可调度的。
函数CreateEvent用来创建事件对象:
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL bManualReset,
BOOL bInitialState,
PCTSTR pszName);
参数psa和pszName的含义已在第3章详细讨论过,这里不再赘述。参数bManualReset告诉系统创建manual-reset(TRUE)还是auto-reset(FALSE)类型的对象,bInitialState用来将事件对象的状态初始化为signaled(TRUE)或unsignaled(FALSE)。系统成功创建对象后,CreateEvent返回进程相关的事件句柄。Windows Vista提供了新函数CreateEventEx创建事件对象:
HANDLE CreateEventEx(
PSECURITY_ATTRIBUTES psa,
PCTSTR pszName,
DWORD dwFlags,
DWORD dwDesiredAccess
);
psa和pszName参数含义同CreateEvent。dwFlags取值如表9-1所示:
常量值(WinBase.h中定义) | 描述 |
---|---|
CREATE_EVENT_NITIAL_SET(0x00000002) | 该值被设置时,新建的事件对象会初始化为signaled,否则为unsignaled |
CREATE_EVENT_MANUAL_RESET(0x00000001) | 该值被设置时,新建的事件对象为manual-reset类型,否则为auto-reset类型 |
dwDesiredAccess参数指定了进程访问创建的事件对象所需的权限,关于访问权限的更多信息请参阅http://msdn2.microsoft.com/en-us/library/ms686670.aspx
线程可以使用多种方式获得其它进程中的线程创建的事件对象:调用CreateEvent并为pszName参数传递已存在对象的名称、使用进程继承、调用DuplicateHandle复制句柄,或调用OpenEvent,并向其传递已存在的对象句柄:
HANDLE OpenEvent(
DWORD dwDesiredAccess,
BOOL bInherit,
PCTSTR pszName);
当不再使用事件对象时,要记得调用CloseHandle将句柄关闭。
事件对象旦创建,你就可以直接控制其状态了。函数SetEvent将事件对象置为signaled状态:
BOOL SetEvent(HANDLE hEvent)
函数ResetEvent将事件对象重置为unsignaled:
BOOL ResetEvent(HANDLE hEvent)
微软为auto-reset类型的事件对象定义了“成功等待的副作用”——当某线程等待该对象成功时,对象状态会自动重置为unsignaled,因此通常没有必要为auto-reset类型的对象调用ResetEvent。要注意,微软并未为manual-reset类型的事件对象定义这种机制。
下面我们举一个很简单的例子,来说明如何使用事件对象同步线程:
HANDLE g_hEvent;
int WINAPI _tWinMain(...){
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
HANDLE hThread[3];
DWORD dwThreadID;
hThread[0] = _beginthreadex(NULL, 0, WordCount, NULL, 0, &dwThreadID);
hThread[1] = _beginthreadex(NULL, 0, SpellCheck, NULL, 0, &dwThreadID);
hThread[2] = _beginthreadex(NULL, 0, GrammarCheck, NULL, 0, &dwThreadID);
OpenFileAndReadContentsIntoMemory(...);
SetEvent(g_hEvent);
}
DWORD WINAPI WordCount(PVOID pvParam){
WaitForSingleObject(g_hEvent, INFINITE);
...
return (0);
}
DWORD WINAPI SpellCheck(PVOID pvParam){
WaitForSingleObject(g_hEvent, INFINITE);
...
return (0);
}
DWORD WANAPI GrammarCheck(PVOID pvParam){
WaitForSingleObject(g_hEvent, INFINITE);
...
return (0);
}
上面代码的含义非常清晰,主线程创建一个manual-reset类型的事件对象和三个线程用于单词计数、语法检查和词法检查,在文件加载到内存之前,事件对象是unsignaled状态,因此调用WaitForSingleObject等待该对象的三个线程均处于等待状态,文件加载完成后,主线程调用SetEvent将事件对象状态变为signaled,此时所有等待该对象的线程都会变成可调度的——这是manual-reset事件对象的重要特性。假如用auto-reset类型的对象替换代码中创建的manual-reset对象,程序的行为将有所不同:auto-reset事件对象变为signaled后,等待该对象的所有线程中只有一个变为可调度的,同时事件对象状态自动重置为unsignaled,以上操作是原子的,因此其余的线程没有机会得知对象状态的改变,它们将继续等待。
函数 BOOL PulseEvent(HANDLE hEvent) 可以将某个事件对象状态变为signaled后马上又置为unsignaled,就像一个脉冲。这个函数不太常用,本章后面讨论SignalObjectAndWait函数时会谈到该函数的更多信息。
以下是使用临界区和Manual-Reset Event对象创建生产者/消费者模型的伪代码:
/* 消费者线程 */
DWORD WINAPI Consumer(PVOID pvParam)
{
while(true){
进入临界区; // 有可能阻塞
while(缓冲区为空 && 未终止){ //已进入临界区
离开临界区;
等待缓冲区非空事件; //有可能阻塞
进入临界区; // 有可能阻塞
}
// 此时缓冲区非空 或 应用要终止
if(终止){
离开临界区;
设置缓冲区非空事件为signaled,以通知其它消费者退出;
跳出最外层while循环;
}
从缓冲区中读取数据;
if(缓冲区为空){
将缓冲区非空事件重设为unsignaled;
}
离开临界区;
设置缓冲区未满事件为signaled,通知生产者可以生产了;
}
}
/* 生产者线程 */
DWORD WINAPI Producer(PVOID pvParam)
{
while(true){
进入临界区; // 有可能阻塞
while(缓冲区已满 && 未终止){ //已进入临界区
离开临界区;
等待缓冲区未满事件; //有可能阻塞
进入临界区; // 有可能阻塞
}
// 此时缓冲区未满 或 应用要终止
if(终止){
离开临界区;
设置缓冲区未满事件为signaled,以通知其它生产退出;
跳出最外层while循环;
}
从缓冲区中写入数据;
if(缓冲区已满){
将缓冲区未满事件重置为unsignaled;
}
离开临界区;
设置缓冲区非空事件为signaled,通知消费者可以读数据了;
}
}