多对象等待的上限
在 Windows 平台下,使用 WaitForMultipleObjects 函数等待多个同步对象(如事件、互斥体、信号量等)时,最多可以同时等待 64 个对象。这一限制由 Windows API 中的常量 MAXIMUM_WAIT_OBJECTS(定义为 64)明确规定。
同时Windows单个进程至少可以创建 16384 个句柄,这是由内核内存池大大小决定的,所以可能有需要等待数百个事件对象的场景发生,如何突破这个限制呢?
第一种方案是创建一个线程来等待 MAXIMUM_WAIT_OBJECTS 句柄,然后等待该线程和其他句柄。 使用此技术可将句柄分解为 MAXIMUM_WAIT_OBJECTS组,这个方案中,两层结构可以保证快速响应的前提下等待 3696 个句柄,而它仅仅比调用WaitForMultipleObjects 多了几个内核切换的动作。
第二种方案则是调用 RegisterWaitForSingleObject 或 SetThreadpoolWait 等待每个句柄。 线程池在句柄上高效等待,并在发出对象信号或超时间隔过期后分配工作线程,本质上它和第一个方案是一回事,只是使用了线程池进行调度罢了。
这个时间仍旧是可以计算的,假设CPU主频是3GHz,一个周期大约是0.33纳秒,1微秒就是大约3000个周期。不过这可能是一个平均估计,而最优情况可能更低。我们可以大概计算一下:
1 模式切换(用户态 ↔ 内核态)
通过系统调用(如syscall或sysenter指令)进入内核态的开销通常在 100~300个时钟周期(假设现代CPU主频为3GHz)。这一过程主要涉及寄存器保存和权限级别的切换,若数据在缓存中,开销可能更低。
2 上下文切换(线程切换)
如果WaitForSingleObject导致线程阻塞并触发调度器切换线程,则需要完整的上下文切换。这包括保存当前线程的寄存器、程序计数器、栈指针等状态,并加载下一个线程的上下文。
在最优情况下,如线程属于同一进程,数据在缓存中,无需TLB刷新,开销可能在 1000~3000个时钟周期。跨进程切换或缓存未命中时,开销可能显著增加,例如达到数万周期。
所以综合考虑,若仅执行系统调用且无需阻塞,例如等待对象已就绪,开销可能接近模式切换的最小值,约100~300周期;若线程被阻塞并切换,则总开销至少需要 1000周期以上(取决于上下文保存/恢复和调度逻辑)。
代码演示
以下示例使用 CreateEvent 函数创建两个事件对象,使用 CreateThread 函数创建线程。 然后,它使用 WaitForMultipleObjects 函数等待线程将其中一个对象的状态设置为使用 SetEvent 函数发出信号。
#include <windows.h>
#include <stdio.h>
HANDLE ghEvents[2];
DWORD WINAPI ThreadProc( LPVOID );
int main( void )
{
HANDLE hThread;
DWORD i, dwEvent, dwThreadID;
// Create two event objects
for (i = 0; i < 2; i++)
{
ghEvents[i] = CreateEvent(
NULL, // default security attributes
FALSE, // auto-reset event object
FALSE, // initial state is nonsignaled
NULL); // unnamed object
if (ghEvents[i] == NULL)
{
printf("CreateEvent error: %d\n", GetLastError() );
ExitProcess(0);
}
}
// Create a thread
hThread = CreateThread(
NULL, // default security attributes
0, // default stack size
(LPTHREAD_START_ROUTINE) ThreadProc,
NULL, // no thread function arguments
0, // default creation flags
&dwThreadID); // receive thread identifier
if( hThread == NULL )
{
printf("CreateThread error: %d\n", GetLastError());
return 1;
}
// Wait for the thread to signal one of the event objects
dwEvent = WaitForMultipleObjects(
2, // number of objects in array
ghEvents, // array of objects
FALSE, // wait for any object
5000); // five-second wait
// The return value indicates which event is signaled
switch (dwEvent)
{
// ghEvents[0] was signaled
case WAIT_OBJECT_0 + 0:
// TODO: Perform tasks required by this event
printf("First event was signaled.\n");
break;
// ghEvents[1] was signaled
case WAIT_OBJECT_0 + 1:
// TODO: Perform tasks required by this event
printf("Second event was signaled.\n");
break;
case WAIT_TIMEOUT:
printf("Wait timed out.\n");
break;
// Return value is invalid.
default:
printf("Wait error: %d\n", GetLastError());
ExitProcess(0);
}
// Close event handles
for (i = 0; i < 2; i++)
CloseHandle(ghEvents[i]);
return 0;
}
DWORD WINAPI ThreadProc( LPVOID lpParam )
{
// lpParam not used in this example
UNREFERENCED_PARAMETER( lpParam);
// Set one event to the signaled state
if ( !SetEvent(ghEvents[0]) )
{
printf("SetEvent failed (%d)\n", GetLastError());
return 1;
}
return 0;
}