Event 内核对象,实际上是解决线程同步问题的利器。
介绍下函数应用:
第一个 CreateEvent
函数功能:创建事件
函数原型:
HANDLECreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes,
BOOL bManualReset,
BOOL bInitialState,
LPCTSTR lpName
);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。
如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。
如果是手动置位,则需要自己调用ResetEvent()使事件变成未触发状态。
第三个参数表示事件的初始状态,传入TRUR表示已触发。
第四个参数表示事件的名称,传入NULL表示匿名事件。
第二个 OpenEvent
函数功能:根据名称获得一个事件句柄。
函数原型:
HANDLEOpenEvent(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName //名称
);
函数说明:
第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示事件句柄继承性,一般传入TRUE即可。
第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。
第三个SetEvent
函数功能:触发事件
函数原型:BOOL SetEvent(HANDLE hEvent);
函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。
第四个ResetEvent
函数功能:将事件设为末触发
函数原型:BOOL ResetEvent(HANDLE hEvent);
最后一个事件的清理与销毁
由于事件是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。
在经典多线程问题中设置一个事件和一个关键段。用事件处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。代码如下:
#include <windows.h> #include <process.h> #include <iostream> using namespace std; const int THREADNUM = 20; volatile long number = 0; HANDLE th_event; CRITICAL_SECTION threadPM,threadCode; unsigned int __stdcall threadFunc(PVOID pM) { int nThreadNum = * ((int *)pM); SetEvent(th_event); Sleep(100); EnterCriticalSection(&threadCode); cout << nThreadNum << endl; number++; Sleep(0); LeaveCriticalSection(&threadCode); return 0; } int main() { int num = 20; HANDLE handle[THREADNUM]; InitializeCriticalSection(&threadCode); number = 0; th_event = CreateEvent(NULL, TRUE, FALSE, NULL); for(int i=0; i< THREADNUM; i++) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, threadFunc, (PVOID) &i, 0, NULL); WaitForSingleObject(th_event, INFINITE); ResetEvent(th_event); } WaitForMultipleObjects(THREADNUM, handle, TRUE ,INFINITE); //安全数量为64 Sleep(500); cout << "计数个数为" << number << endl; CloseHandle(th_event); DeleteCriticalSection(&threadCode); getchar(); return 0; }
上述代码是事件对象的手动置位,
下面的两行尤为重要:
运行效果如下:
如果在手动条件下,忘记对事件重新置位,即忘记写resetevent函数,则同步将没有效果,运行如下:
在自动置位的条件下,则不用书写resetEvent函数,将TRUE改为FALSE即可。
接下来还有一个函数值得注意:
第五个PulseEvent
函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。
函数原型:BOOL PulseEvent(HANDLE hEvent);
函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:
1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。
2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。
此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。
手动置位:代码如下
主线程,启动三个快线程和两个慢线程。
在慢线程创建好以后,故意停顿50毫秒,目的是为了让快线程全部进入对于事件的等待状态,对于事件脉冲有一个充分的感知。
主线程启动所有子线程后再Sleep(50)保证有3个快线程都正处于等待状态中。此时若主线程触发一个事件脉冲,那么对于手动置位事件,这3个线程都将顺利执行下去。对于自动置位事件,这3个线程中会有中一个顺利执行下去。而不论手动置位事件还是自动置位事件,那2个慢线程由于Sleep(100)所以会错过事件脉冲,因此慢线程都会进入等待状态而无法顺利执行下去。
#include <iostream> #include <windows.h> #include <process.h> using namespace std; HANDLE th_event; unsigned int __stdcall faseThreadFunc(PVOID pm) { char* s = (char *) pm; cout << "进入快线程......" << endl; Sleep(10); WaitForSingleObject(th_event, INFINITE); cout << s << endl; return 0; } unsigned int __stdcall slowThreadFunc(PVOID pm) { char* s = (char *) pm; cout << "进入慢线程......" << endl; Sleep(100); WaitForSingleObject(th_event, INFINITE); cout << s << endl; return 0; } int main() { bool ev_Manual = true; if(ev_Manual) cout << "手动测试状态" << endl; else cout << "自动测试状态" << endl; HANDLE th_handle1[3]; HANDLE th_handle2[2]; char s1[3][10] = {"快线程1", "快线程2", "快线程3"}; char s2[2][10] = {"慢线程1", "慢线程2"}; int i; th_event = CreateEvent(NULL, ev_Manual, FALSE, NULL); for(i=0; i<3; i++) { th_handle1[i] = (HANDLE) _beginthreadex(NULL, 0, faseThreadFunc, (PVOID) s1[i], 0, NULL); } for(i=0; i<2; i++) { th_handle2[i] = (HANDLE) _beginthreadex(NULL, 0, slowThreadFunc, (PVOID) s2[i], 0, NULL); } Sleep(50); cout << "现在主线程触发一个事件脉冲" << endl; PulseEvent(th_event); getchar(); return 0; }
运行结果如下:
自动置位:
只是将ev_Manual 改为 false就好;
运行结果如下:
最后总结下事件Event
1.事件是内核对象,事件分为手动置位事件和自动置位事件。事件Event内部它包含一个使用计数(所有内核对象都有),一个布尔值表示是手动置位事件还是自动置位事件,另一个布尔值用来表示事件有无触发。
2.事件可以由SetEvent()来触发,由ResetEvent()来设成未触发。还可以由PulseEvent()来发出一个事件脉冲。
3.事件可以解决线程间同步问题,因此也能解决互斥问题。