使用 Win32 事件对象进行线程同步






4.43/5 (4投票s)
使用 Win32 事件对象进行线程同步。
引言
C/C++ 程序可以在多种情况下使用 Win32 事件对象来通知等待的线程事件的发生。例如,文件、命名管道和通信设备的重叠 I/O 操作使用事件对象来信号表示其完成。
Win32 事件
Win32 事件的工作方式类似于状态机,它在信号状态和非信号状态之间切换。事件处于信号状态意味着它可以释放等待此事件被信号的线程。事件处于非信号状态意味着它不会释放任何等待此特定事件的线程。事件有两种类型:手动重置事件和自动重置事件。手动事件的用户设置需要手动将其置为非信号状态,并使用 ResetEvent
函数进行重置。自动重置事件在对象被置为非信号状态时会自动发生。
CreateEvent
函数用于创建事件线程同步对象。手动重置事件或自动重置事件的选择在 CreateEvent
函数的参数初始化中指定。Wait 系列函数(WaitForSingleObject
、WaitForMultipleObjects
)用于等待特定事件的发生。这些函数可以等待多个对象:单个对象被信号,或者线程中的所有事件都被信号。该函数可以创建命名和未命名事件对象。SetEvent
函数用于将事件对象置为信号状态。ResetEvent
函数用于将事件对象置为非信号状态。如果函数成功,它将返回该事件的句柄。如果命名事件已存在,GetLastError
函数将返回 ERROR_ALREADY_EXISTS
标志。如果命名事件已存在,则使用 OpenEvent
函数来访问之前由 CreateEvent
函数创建的事件。
代码描述
下面的示例使用事件对象演示子线程如何信号事件以通知主线程其完成。主线程正在等待多个线程,以确保所有线程都完成后再继续执行。
// Create an Manual Reset Event where events must be reset
// manually to non signaled state
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(...)
。 - 第四个参数是事件的名称,它将用于全局标识。如果已存在同名的事件,则会打开现有事件的句柄。
// Array to store thread handles
HANDLE Array_Of_Events_Handles[3];
// create a Thread which will wait for the events to occur
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;
// Wait until all events are signaled.
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;
// Get the handler to the event for which we need to wait in
// this thread.
HANDLE hEvent = OpenEvent ( EVENT_ALL_ACCESS , false, L"MyEvent1" );
if ( !hEvent ) { return -1; }
Sleep ( 5000 );
ResetEvent ( hEvent );
// Signal the event
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
个对象句柄。请参阅此 博客。在某些实际场景中,创建许多线程似乎并非没有风险,可能会存在线程同步开销。尤其是当需要等待的句柄数量非常多,并且所有句柄都必须被信号才能让等待的线程继续前进时。我编写了我自己对 WaitForAllObjects
的扩展,它解决了这个问题,并且在需要信号和等待超过 64 个句柄的场景中很有用,即 'bWaitAll = TRUE
'。
DWORD MyWaitForMultipleObjects (DWORD _NumHandles, HANDLE* _Handles,
BOOL bWaitAll = TRUE, DWORD dwTimeoutMS = INFINITE )
{
HRESULT hr = S_OK;
DWORD dwWaitResult = WAIT_FAILED;
if ((_NumHandles > MAXIMUM_WAIT_OBJECTS) && !bWaitAll)
{
cout << "MyWaitForMultipleObjects: WARNING: too many objects"
<< _NumHandles << MAXIMUM_WAIT_OBJECTS;
return dwWaitResult;
}
// Create tracking array
// Handles will be removed as they are signaled
DWORD NumHandles = _NumHandles;
HANDLE* Handles = new HANDLE [_NumHandles + 1];
for (DWORD i = 0; i < NumHandles; i++)
{
Handles[i] = _Handles[i];
}
bool bDone = false;
bool bTimeout = false;
do
{
dwWaitResult =
WaitForMultipleObjects(min(NumHandles, (MAXIMUM_WAIT_OBJECTS)),
Handles, /*bWaitAll*/false, INFINITE);
if (dwWaitResult == WAIT_FAILED)
{
cout<<"WAIT_FAILED(MyWaitForMultipleObjects)"<<endl;
}
else if (dwWaitResult == WAIT_TIMEOUT)
{
//timeout - we're done
bTimeout = true;
}
else if ((dwWaitResult >= WAIT_OBJECT_0) &&
(dwWaitResult <= (WAIT_OBJECT_0 + NumHandles - 1)))
{
// A Handle has signaled
if (bWaitAll)
{
// remove the handle from the array
DWORD Index = dwWaitResult - WAIT_OBJECT_0;
memmove(&Handles[Index], &Handles[Index+1],
(NumHandles-Index)*sizeof(HANDLE));
Handles[NumHandles-1] = 0;
NumHandles--;
if (NumHandles == 0)
{
bDone = true;
dwWaitResult = WAIT_OBJECT_0;
}
}
else
{
// we are done since bWaitAll is not set
bDone = true;
}
}
} while (!bDone && SUCCEEDED(hr));
delete [] Handles;
return dwWaitResult;
}
上面的代码基本上是在一个名为 'Handles
' 的数组中为前 64 个句柄循环执行 WaitForMultipleObjects
。每当事件/句柄发出信号时,它们就会从 'Handles
' 数组中移除,剩余的句柄会被推入数组。此过程一直持续到所有句柄都被等待并发出信号。
历史
- 2011/3/11:初始发布。
- 2011/4/11:首次更新 - 根据评论,修正了用于保存事件句柄的数组。