上一节我们介绍了线程同步、以及利用互斥对象实现线程同步的方法。这一节,我们继续介绍另一种线程同步的方法:事件对象。如果对线程概念、互斥对象概念不清楚的同学,请查看Windows中多线程的基础知识——1互斥对象。
1 事件对象
1.1 事件对象概念
事件对象也属于内核对象,它包含以下三个成员:
1. 使用计数;
2. 用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值;
3. 用于指明该事件处于已通知状态还是未通知状态的布尔值。
事件对象有两种不同的类型:人工重置的事件对象和自动重置的事件对象。当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程。当一个自动重置的事件对象得到通知时,等待该事件对象的线程只有一个线程变为可调度对象。
1.2 事件对象相关函数
一、创建事件对象
创建事件对象的函数原型为:
HANDLE WINAPI CreateEvent(
__in_opt LPSECURITY_ATTRIBUTES lpEventAttributes,
__in BOOL bManualReset,
__in BOOL bInitialState,
__in_opt LPCTSTR lpName
);
函数参数说明:
lpEventAttributes: 一般为NULL;
bManualReset:创建的Event是人工重置事件对象,还是自动重置事件对象。如果TRUE,表示该函数将创建一个人工重置对象;如果此参数为FALSE,为自动重置事件对象。如果是人工重置事件对象,当线程等待到该对象的所有权之后,需要调用ResetEvent函数手动的将事件对象设置为无信号状态;如果是自动重置对象,当线程等待该对象的所有权之后,系统会自动将对象设置为无信号状态。
bInitialState:初始状态,true,有信号,false无信号
lpName:事件对象的名称。
该函数创建一个Event同步对象,如果CreateEvent调用成功的话,会返回新生成的对象的句柄,否则返回NULL。
请注意,当人工重置的事件对象得到通知时,等待该事件对象的所有线程均变为可调度线程;当一个自动重置的事件对象得到通知时,等待该事件对象的线程中只有一个线程可变为可调度对象,同时操作系统会将该事件对象设置为无信号状态。
MSDN的原文说明为:
When the state of a manual-reset event object is signaled, it remains signaled until it is explicitly reset to nonsignaled by the ResetEvent function. Any number of waiting threads, or threads that subsequently begin wait operations for the specified event object, can be released while the object’s state is signaled.
When the state of an auto-reset event object is signaled, it remains signaled until a single waiting thread is released; the system then automatically resets the state to nonsignaled. If no threads are waiting, the event object’s state remains signaled.
二、设置事件对象状态
SetEvent函数将把指定的事件对象设置为有信号状态,函数原型为:
BOOL WINAPI SetEvent( __in HANDLE hEvent);
其中的参数为将要设置其状态的事件对象的句柄。
三、重置事件对象状态
ResetEvent函数将把指定的事件对象设置为无信号状态,函数原型为:
BOOL WINAPI ResetEvent( __in HANDLE hEvent);
2 利用事件对象实现线程同步的例程
#include <windows.h>
#include <iostream>
HANDLE g_hEvent;
int iTickets = 1000;
//线程1的入口函数
DWORD WINAPI Fun1Proc(__in LPVOID lpParameter)
{
while (true)
{
//请求事件对象
WaitForSingleObject(g_hEvent, INFINITE);
if (iTickets > 0)
{
std::cout << "thread1 sell tickes:" << iTickets-- << std::endl;
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
}
return 0;
}
//线程2的入口函数
DWORD WINAPI Fun2Proc(__in LPVOID lpParameter)
{
while (true)
{
//请求事件对象
WaitForSingleObject(g_hEvent, INFINITE);
if (iTickets > 0)
{
std::cout << "thread2 sell tickes:" << iTickets-- << std::endl;
SetEvent(g_hEvent);
}
else
{
SetEvent(g_hEvent);
break;
}
}
return 0;
}
int main()
{
HANDLE hThread1;
HANDLE hThread2;
//创建人工重置事件内核对象
g_hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
//将事件设置为有信号状态
SetEvent(g_hEvent);
//创建线程
hThread1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
hThread2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread1);
CloseHandle(hThread2);
//以下代码为了防止主线程提前退出,从而使进程退出,也就终止了进程下所有线程的运行
while (true)
{
if (iTickets > 0)
Sleep(200);
else
{
//关闭事件对象句柄
CloseHandle(g_hEvent);
getchar();
return 0;
}
}
}
和上一节一样,以上代码也是在main中创建了两个线程(并且即刻执行),两个线程都会访问全局变量iTickets ,每卖一张票,就会将此全局变量递减1。为了实现线程同步,我们在main中创建了事件对象,并且设置事件对象为自动重置事件对象,且设置其处于有信号状态。在两个线程执行时,会请求这个事件对象,首先得到事件对象的线程得以继续执行,与此同时设置事件对象为无信号状态(因此,其他线程就得不到该事件对象,也就只能暂停等待),当执行完毕时,恢复事件对象为有信号状态。接下来,在系统调度下,两个线程会相继执行,无论哪个线程得到事件对象,都会暂停另一个线程的执行,这样,就实现了线程同步。