事件是一个允许一个线程在某种情况发生时,唤醒另外一个线程的同步对象,或者说当应用程序必须等到发生某事才能访问资源,应该使用事件对象。
在所有的内核对象中,事件内核对象是个最基本的对象。它们包含一个使用计数(与所有内核对象一样),一个用于指明该事件是个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
事件能够通知一个操作已经完成。有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程,需要调用ResetEvent事件才置为未触发状态。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程,同时系统将自动事件重置为未触发状态。
当一个线程执行初始化操作,然后通知另一个线程执行剩余的操作时,事件使用得最多。事件初始化为未通知状态,然后,当该线程完成它的初始化操作后,它就将事件设置为已通知状态。这时,一直在等待该事件的另一个线程发现该事件已经得到通知,因此它就变成可调度线程。这第二个线程知道第一个线程已经完成了它的操作。
1、 以SDK方式使用事件同步对象
下面是CreateEvent函数,用于创建事件内核对象:
HANDLE CreateEvent(
PSECURITY_ATTRIBUTES psa,
BOOL fManualReset,
BOOL fInitialState,
PCTSTR pszName);
psa参数,用于创建内核对象的函数几乎都有一个指向SECURITY_ATTRIBUTES 结构的指针作为其参数,大多数应用程序只是为该参数传递NULL ,这样就可以创建带有默认安全性的内核对象。默认安全性意味着对象的管理小组的任何成员和对象的创建者都拥有对该对象的全部访问权,而其他所有人均无权访问该对象。但是,可以指定一个SECURITY_ATTRIBUTES结构,对它进行初始化,并为该参数传递该结构的地址。SECURITY_ATTRIBUTES结构类似下面的样子:
typedef struct _SECURITY_ATTRIBUTES
{
DWORD nLength,
LPVOID lpSecurityDescriptor;
BOOL bInherttHandle;
} SECURITY_ATTRIBUTES;
尽管该结构称为SECURITY_ATTRIBUTES,但是它包含的与安全性有关的成员实际上只有一个,即lpSecurityDescriptor。如果你想要限制人们对你创建的内核对象的访问,必须创建一个安全性描述符,然后像下面这样对SECURITY_ATTRIBUTES结构进行初始化:
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa); //Used for versioning
sa.lpSecuntyDescriptor = pSD, //Address of an initialized SD
sa.bInheritHandle = FALSE; //Discussed later
HANDLE hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE,
&sa, PAGE_REAOWRITE, 0, 1024, "MyFileMapping");
fManualReset参数是个布尔值,它能够告诉系统是创建一个人工重置的事件(TRUE)还是创建一个自动重置的事件(FALSE)。
fInitialState参数用于指明该事件是要初始化为已通知状态(TRUE)还是未通知状态(FALSE)。当系统创建事件对象后,CreateEvent就将与进程相关的句柄返回给事件对象。其他进程中的线程可以获得对该对象的访问权,方法是使用在pszName参数中传递的相同值。
与所有情况中一样,当不再需要事件内核对象时,应该调用CloseHandle函数。
一旦事件已经创建,就可以直接控制它的状态。当调用SetEvent时,可以将事件改为已通知状态:
BOOL SetEvent(HANDLE hEvent);
当调用ResetEvent函数时,可以将该事件改为未通知状态:
BOOL ResetEvent(HANDLE hEvent);
事件的使用示例如下:
HANDLE g_hEvent;
int WINAPI WinMain(...)
{
g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
HANDLE hThread[3];
DWORD dwThreadID;
hThread[0] = _beginthreadex(NULL, 0, ThreadProc1, NULL, 0, &dwThreadID);
hThread[1] = _beginthreadex(NULL, 0, ThreadProc2, NULL, 0, &dwThreadID);
hThread[2] = _beginthreadex(NULL, 0, ThreadProc3, NULL, 0, &dwThreadID);
... //主线程要做的初始化工作
SetEvent(g_hEvent);
}
DWORD WINAPI ThreadProc1(PVOID pvParam)
{
WaitForSingleObject(g_hEvent, INFINITE);
...
return(0);
}
DWORD WINAPI ThreadProc2(PVOID pvParam)
{
WaitForSingleObject(g_hEvent, INFINITE);
...
return(0);
}
DWORD WINAPI ThreadProc3(PVOID pvParam)
{
WaitForSingleObject(g_hEvent, INFINITE);
...
return(0);
}
当这个进程启动时,它创建一个人工重置的未通知状态的事件,并且将句柄保存在一个全局变量中。这使得该进程中的其他线程能够非常容易地访问同一个事件对象。现在3个线程已经产生,这些线程要等待主线程的初始化工作完成。这3个线程函数的代码的开始部分都相同,每个函数都调用WaitForSingleObject,这将使线程暂停运行,直到主线程主线程调用SetEvent为止。
一旦主线程将数据准备好,它就调用SetEvent,给事件发出通知信号。这时,系统就使所有这3个辅助线程进入可调度状态,它们都获得了CPU时间。注意,这3个线程都以只读方式访问主线程初始化好的内存。这就是所有3个线程能够同时运行的唯一原因。
如果你使用自动重置的事件而不是人工重置的事件,那么应用程序的行为特性就有很大的差别。当主线程调用SetEvent之后,系统只允许一个辅助线程变成可调度状态。同样,也无法保证系统将使哪个线程变为可调度状态。其余两个辅助线程将继续等待。
让我们重新编写线程的函数,使得每个函数在返回前调用SetEvent函数(就像WinMain函数所做的那样)。这些线程函数现在变成下面的形式:
DWORD WINAPI ThreadProc1(PVOID pvParam)
{
WaitForSingleObject(g_hEvent, INFINITE);
...
SetEvent(g_hEvent);
return(0);
}
DWORD WINAPI ThreadProc2(PVOID pvParam)
{
WaitForSingleObject(g_hEvent, INFINITE);
...
SetEvent(g_hEvent);
return(0);
}
DWORD WINAPI ThreadProc3(PVOID pvParam)
{
WaitForSingleObject(g_hEvent, INFINITE);
...
SetEvent(g_hEvent);
return(0);
}
当线程完成它对数据的专门传递时,它就调用SetEvent函数,该函数允许系统使得两个正在等待的线程中的一个成为可调度线程。同样,我们不知道系统将选择哪个线程作为可调度线程,但是该线程将进行它自己的对内存块的专门传递。当该线程完成操作时,它也将调用SetEvent函数,使第三个即最后一个线程进行它自己的对内存块的传递。注意,当使用自动重置事件时,如果每个辅助线程均以读/写方式访问内存块,那么就不会产生任何问题,这些线程将不再被要求将数据视为只读数据。这个例子清楚地展示出使用人工重置事件与自动重置事件之间的差别。
2、 使用MFC的CEvent类
CEvent 类的各成员函数的原型和参数说明如下:
1)CEvent(BOOL bInitiallyOwn=FALSE,
BOOL bManualReset=FALSE,
LPCTSTR lpszName=NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);
bInitiallyOwn:指定事件对象初始化状态,TRUE为有信号,FALSE为无信号;
bManualReset:指定要创建的事件是属于人工事件还是自动事件。TRUE为人工事件,FALSE为自动事件;
后两个参数一般设为NULL,在此不作过多说明。
2)BOOL CEvent::SetEvent();
将 CEvent 类对象的状态设置为有信号状态。如果事件是人工事件,则 CEvent 类对象保持为有信号状态,直到调用成员函数ResetEvent()将其重新设为无信号状态时为止。如果CEvent 类对象为自动事件,则在SetEvent()将事件设置为有信号状态后,CEvent 类对象由系统自动重置为无信号状态。
如果该函数执行成功,则返回非零值,否则返回零。
3)BOOL CEvent::ResetEvent();
该函数将事件的状态设置为无信号状态,并保持该状态直至SetEvent()被调用时为止。由于自动事件是由系统自动重置,故自动事件不需要调用该函数。如果该函数执行成功,返回非零值,否则返回零。
一般通过调用WaitForSingleObject函数来监视事件状态。