在多线程编程中,经常需要在某些情况下通知等待线程事件的发生。例如,在文件、命名管道和通信设备上进行的重叠I/O操作会使用事件对象来标志它们的完成。Win32事件对象类似于状态机,其生命周期在两种状态之间转换:信号状态和非信号状态。当事件处于信号状态时,它有能力释放等待该事件被信号化的线程。当事件处于非信号状态时,它不会释放任何等待特定事件的线程。事件有两种类型:手动重置事件和自动重置事件。手动重置事件需要用户手动将其设置为非信号状态,使用ResetEvent
函数。自动重置事件在对象被提升到非信号状态时自动发生。
CreateEvent
函数用于创建事件线程同步对象。手动或自动重置事件的选择在CreateEvent
函数参数初始化中提到。等待事件族函数(WaitForSingleObject
, WaitForMultipleObjects
)用于等待特定事件发生。一组对象等待事件:单个对象被信号化或整个事件在线程中被信号化。这个函数可以创建命名和未命名的事件对象。SetEvent
函数用于将事件对象设置为信号状态。ResetEvent
函数用于将事件对象设置为非信号状态。如果函数成功,它返回该事件的句柄。如果命名事件已经存在,GetLastError
函数返回ERROR_ALREADY_EXISTS
标志。如果命名事件已经存在,OpenEvent
函数用于访问之前由CreateEvent
函数创建的事件。
以下示例使用事件对象来演示子线程如何通过信号事件来通知主线程其完成情况。等待多个线程的主线程确保所有线程完成之前不会执行进一步的操作。
// 创建一个手动重置事件,事件必须手动重置为非信号状态
HANDLE hEvent1 = CreateEvent(NULL, true, false, L"MyEvent1");
if (!hEvent1)
return -1;
HANDLE hEvent2 = CreateEvent(NULL, true, false, L"MyEvent2");
if (!hEvent2)
return -1;
HANDLE hEvent3 = CreateEvent(NULL, true, false, L"MyEvent3");
if (!hEvent3)
return -1;
分别创建了三个事件"MyEvent1"、"MyEvent2"和"MyEvent3"。使用CreateEvent(NULL, true, false, "MyEvent");
来创建一个事件。参数描述如下:
第一个参数NULL
代表默认的安全属性。
第二个参数是手动重置事件的标志。
false
表示事件将是自动重置事件,如果是true
则是手动重置事件。
第三个参数是事件创建时的状态标志。如果为false
,事件将被创建在非信号状态,如果为true
,事件将被创建在信号状态。在自动重置事件的情况下,如果事件在信号状态被创建,那么第一个等待信号的线程将在没有调用SetEvent(...)
的情况下被释放。在手动重置事件的情况下,除非调用ResetEvent(...)
,否则所有等待此信号的线程都将被释放。
第四个参数是事件的名称,它将被全局识别。如果已经存在具有相同名称的事件,则会打开现有事件的句柄。
// 存储事件句柄的数组
HANDLE Array_Of_Events_Handles[3];
// 创建一个线程,等待事件的发生
DWORD Id;
HANDLE hThrd1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFun1, 0, 0, &Id);
if (!hThrd1) { CloseHandle(hEvent1); return -1; }
HANDLE hThrd2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFun2, 0, 0, &Id);
if (!hThrd2) { CloseHandle(hEvent2); return -1; }
HANDLE hThrd3 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFun3, 0, 0, &Id);
if (!hThrd3) { CloseHandle(hEvent3); return -1; }
Array_Of_Events_Handles[0] = hEvent1;
Array_Of_Events_Handles[1] = hEvent2;
Array_Of_Events_Handles[2] = hEvent3;
// 等待所有事件被信号化。
while (1) {
//WaitForMultipleObjects(3, Array_Of_Events_Handles, TRUE, 20000);
MyWaitForMultipleObjects(3, Array_Of_Events_Handles, TRUE, 20000);
break;
}
启动了三个独立的线程,它们各自的方法将通过在执行时睡眠5秒、10秒、15秒来模拟它们的任务。事件句柄存储在数组Array_Of_Events_Handles
中,以便主线程可以等待这些句柄。
DWORD WINAPI ThreadFun1(LPVOID n) {
cout << "Thread Instantiated 1........." << endl;
// 获取此线程需要等待的事件的句柄。
HANDLE hEvent = OpenEvent(EVENT_ALL_ACCESS, false, L"MyEvent1");
if (!hEvent) {
return -1;
}
Sleep(5000);
ResetEvent(hEvent);
// 信号事件
if (SetEvent(hEvent)) {
cout << "Got The signal - MyEvent 1......." << endl;
}
CloseHandle(hEvent);
cout << "End of the Thread 1......" << endl;
return 0;
}
上面的代码是其中一个线程函数的示例。这基本上打开了已经创建的事件,并在睡眠5秒后手动重置它。
由于在等待所有三个线程句柄直到通过'ResetEvent(hEvent)
'发出信号,线程是同步的,主线程在它们完成之前等待它们的完成,然后才能继续执行。输出充满了控制台日志,以给一种事件顺序的感觉。
'WaitForMultipleObjects
'限制为等待MAXIMUM_WAIT_OBJECTS
句柄计数,即64。如果需要等待超过MAXIMUM_WAIT_OBJECTS
句柄,可以创建一个单独的线程来等待MAXIMUM_WAIT_OBJECTS
,然后等待这些线程完成。使用这种方法,可以创建MAXIMUM_WAIT_OBJECTS
线程,每个线程都可以等待MAXIMUM_WAIT_OBJECTS
对象句柄。参考这个blog
。在一些实际场景中,创建许多线程似乎并不是无风险的,可能会有线程同步开销。特别是当有太多需要等待的句柄,并且所有句柄都需要在等待线程可以继续执行之前被信号化时。编写了自己的WaitForAllObjects
的扩展,它解决了这个问题,并且在需要超过64个句柄被信号化和等待的场景中很有用,即'bWaitAll = TRUE
'.