探讨WaitForMultipleObjects如何突破64个句柄的限制

DWORD WINAPI WaitForMultipleObjects(
  __in          DWORD nCount,
  __in          const HANDLE* lpHandles,
  __in          BOOL bWaitAll,
  __in          DWORD dwMilliseconds
);

更多关于该函数的详细信息,请参看MSDN地址:https://msdn.microsoft.com/en-us/library/windows/desktop/ms687025(v%3Dvs.85).aspx。众所周知,函数WaitForMultipleObjects最多只能等待64个句柄,那么如果句柄数量多于64个的时候,我们该如何处理呢?首先总结一下查阅资料时,网友给出的一些说法吧。


1.修改程序设计


尴尬对于该方法,现在不作过多讨论。既然这里提出了要突破WaitForMultipleObjects只能等到64个HANDLE的限制,自然有我们提出的原因和意义。


2.大众简易版


哈哈哈,这里之所以叫大众简易版,是因为这个方法在网上随便搜索就能搜索出来,而且实现也简单。直接上代码:

void WaitForMultipleObjectsExpand(HANDLE * handles, DWORD count)
{
	DWORD index = 0;
	DWORD result = 0;
	DWORD handleCount = count;

	/// 每64个HANDLE分为一组
	while (handleCount >= MAXIMUM_WAIT_OBJECTS)
	{
		WaitForMultipleObjects(MAXIMUM_WAIT_OBJECTS, &handles[index], TRUE, INFINITE);

		handleCount -= MAXIMUM_WAIT_OBJECTS;
		index += MAXIMUM_WAIT_OBJECTS;
	}

	if (handleCount > 0)
	{
		WaitForMultipleObjects(handleCount, &handles[index], TRUE, INFINITE);
	}
}
这种方法是可以等待无限个数的句柄,不过弊端也很明显,撇开效率的问题不说,单是在使用上就存在以下限制:

  • 无法设置超时时间,必须无限等待
  • 只能等待全部句柄处于有信号状态
鉴于以上的限制,该方法相比于原函数WaitForMultipleObjects的功能显得十分逊色。

3.《Windows核心编程》版

在查阅资料时,有网友说Windows核心编程里实现了一个WaitForMultipleExpressions函数,可以等待远远超过64个的句柄数量。我自己手上刚好有第五版的《Windows核心编程》,为什么我对这个函数没有印象?原来这个函数是在第四版《Windows核心编程》的第10章里实现的,到第五版的时候这部分内容就没有了……为了方便,这里把书本上的WaitForMultipleExpressions源码整理出来,关于该函数的完整实现和示例源码请点击下方链接下载:

WaitForMultipleExpressions函数的完整实现和示例源码下载
/******************************************************************************
Module:  WaitForMultExp.cpp
Notices: Copyright (c) 2000 Jeffrey Richter
******************************************************************************/


#include "..\CmnHdr.h"     /* See Appendix A. */
#include <malloc.h>
#include <process.h>
#include "WaitForMultExp.h"


///////////////////////////////////////////////////////////////////////////////


// Internal data structure representing a single expression.
// Used to tell OR-threads what objects to wait on.
typedef struct {
   PHANDLE m_phExpObjects;   // Points to set of handles
   DWORD   m_nExpObjects;    // Number of handles
} EXPRESSION, *PEXPRESSION;


///////////////////////////////////////////////////////////////////////////////


// The OR-thread function
DWORD WINAPI WFME_ThreadExpression(PVOID pvParam) {

   // This thread function just waits for an expression to come true. 
   // The thread waits in an alertable state so that it can be forced
   // to stop waiting by queuing an entry to its APC queue.
   PEXPRESSION pExpression = (PEXPRESSION) pvParam;
   return(WaitForMultipleObjectsEx(
      pExpression->m_nExpObjects, pExpression->m_phExpObjects, 
      FALSE, INFINITE, TRUE));
}


///////////////////////////////////////////////////////////////////////////////


// This is the APC callback routine function
VOID WINAPI WFME_ExpressionAPC(ULONG_PTR dwData) {

   // This function intentionally left blank
}


///////////////////////////////////////////////////////////////////////////////


// Function to wait on mutiple Boolean expressions
DWORD WINAPI WaitForMultipleExpressions(DWORD nExpObjects, 
   CONST HANDLE* phExpObjects, DWORD dwMilliseconds) {

   // Allocate a temporary array because we modify the passed array and
   // we need to add a handle at the end for the hsemOnlyOne semaphore.
   PHANDLE phExpObjectsTemp = (PHANDLE)
      _alloca(sizeof(HANDLE) * (nExpObjects + 1));
   CopyMemory(phExpObjectsTemp, phExpObjects, sizeof(HANDLE) * nExpObjects);
   phExpObjectsTemp[nExpObjects] = NULL;  // Put sentinel at end

   // Semaphore to guarantee that only one expression gets satisfied
   HANDLE hsemOnlyOne = CreateSemaphore(NULL, 1, 1, NULL);
   
   // Expression information: 1 per possible thread
   EXPRESSION Expression[MAXIMUM_WAIT_OBJECTS];

   DWORD dwExpNum  = 0;    // Current expression number
   DWORD dwNumExps = 0;    // Total number of expressions

   DWORD dwObjBegin = 0;   // First index of a set
   DWORD dwObjCur   = 0;   // Current index of object in a set

   DWORD dwThreadId, dwWaitRet = 0;

   // Array of thread handles for threads: 1 per expression
   HANDLE ahThreads[MAXIMUM_WAIT_OBJECTS];

   // Parse the callers handle list by initializing a structure for
   // each expression and adding hsemOnlyOne to each expression.
   while ((dwWaitRet != WAIT_FAILED) && (dwObjCur <= nExpObjects)) {

      // While no errors, and object handles are in the caller's list...

      // Find next expression (OR-expressions are separated by NULL handles)
      while (phExpObjectsTemp[dwObjCur] != NULL) 
         dwObjCur++;
      
      // Initialize Expression structure which an OR-thread waits on
      phExpObjectsTemp[dwObjCur] = hsemOnlyOne;
      Expression[dwNumExps].m_phExpObjects =  &phExpObjectsTemp[dwObjBegin];
      Expression[dwNumExps].m_nExpObjects  =  dwObjCur - dwObjBegin + 1;

      if (Expression[dwNumExps].m_nExpObjects > MAXIMUM_WAIT_OBJECTS) {
         // Error: Too many handles in single expression
         dwWaitRet = WAIT_FAILED;
         SetLastError(ERROR_SECRET_TOO_LONG);
      }

      // Advance to the next expression
      dwObjBegin = ++dwObjCur;
      if (++dwNumExps == MAXIMUM_WAIT_OBJECTS) {
         // Error: Too many expressions
         dwWaitRet = WAIT_FAILED;
         SetLastError(ERROR_TOO_MANY_SECRETS);
      }
   }

   if (dwWaitRet != WAIT_FAILED) {

      // No errors occurred while parsing the handle list

      // Spawn thread to wait on each expression
      for (dwExpNum = 0; dwExpNum < dwNumExps; dwExpNum++) {

         ahThreads[dwExpNum] = chBEGINTHREADEX(NULL, 
            1, // We only require a small stack
            WFME_ThreadExpression, &Expression[dwExpNum], 
            0, &dwThreadId);
      }

      // Wait for an expression to come TRUE or for a timeout
      dwWaitRet = WaitForMultipleObjects(dwExpNum, ahThreads, 
         FALSE, dwMilliseconds);

      if (WAIT_TIMEOUT == dwWaitRet) {

         // We timed-out, check if any expressions were satisfied by 
         // checking the state of the hsemOnlyOne semaphore.
         dwWaitRet = WaitForSingleObject(hsemOnlyOne, 0);

         if (WAIT_TIMEOUT == dwWaitRet) {

            // If the semaphore was not signaled, some thread expressions
            // was satisfied; we need to determine which expression.
            dwWaitRet = WaitForMultipleObjects(dwExpNum, 
               ahThreads, FALSE, INFINITE);

         } else {

            // No expression was satisfied and WaitForSingleObject just gave
            // us the semaphore so we know that no expression can ever be 
            // satisfied now -- waiting for an expression has timed-out.
            dwWaitRet = WAIT_TIMEOUT;
         }
      }

      // Break all the waiting expression threads out of their 
      // wait state so that they can terminate cleanly.
      for (dwExpNum = 0; dwExpNum < dwNumExps; dwExpNum++) {

         if ((WAIT_TIMEOUT == dwWaitRet) || 
             (dwExpNum != (dwWaitRet - WAIT_OBJECT_0))) {

            QueueUserAPC(WFME_ExpressionAPC, ahThreads[dwExpNum], 0);
         }
      }

#ifdef _DEBUG
      // In debug builds, wait for all of expression threads to terminate 
      // to make sure that we are forcing the threads to wake up. 
      // In non-debug builds, we'll assume that this works and 
      // not keep this thread waiting any longer.
      WaitForMultipleObjects(dwExpNum, ahThreads, TRUE, INFINITE);
#endif

      // Close our handles to all the expression threads
      for (dwExpNum = 0; dwExpNum < dwNumExps; dwExpNum++) {
         CloseHandle(ahThreads[dwExpNum]);
      }
   }  // error occurred while parsing

   CloseHandle(hsemOnlyOne);
   return(dwWaitRet);
}


//////////////////////////////// End of File //////////////////////////////////
通过源代码,我们可以发现WaitForMultipleExpressions函数和WaitForMultipleObjects函数在功能上存在以下几点不同:
  • WaitForMultipleObjects函数最多可以等待64个句柄,而WaitForMultipleExpressions函数最多可以等待64个表达式。这里的表达式其实是由句柄组成的,一个表达式最多可以包含63个句柄,多个表达式之间用NULL句柄隔开。所以,WaitForMultipleExpressions函数最多可以等待 64*63=4032个句柄,前提是这4032个句柄必须分为64个组,每个组中包含63个句柄。
  • WaitForMultipleObjects函数可以选择等待其中一个句柄,或者选择等待全部句柄为有信号状态,而WaitForMultipleExpressions函数只能选择等待其中一个表达式为有信号状态,而不能选择等待全部表达式。一个表达式是否为有信号状态,取决于表达式中包含的句柄是否全部处于有信号状态。若表达式包含的句柄全部处于有信号状态,则该表达式也处于有信号状态;否则,该表达式处于无信号状态。
  • WaitForMultipleObjects函数的返回值可以指明是哪个句柄处于有信号状态,而WaitForMultipleExpressions函数的返回值则可以指明哪个表达式处于有信号状态,即指明该表达式中的句柄全部为有信号状态。
  • 最重要的一点,WaitForMultipleObjects函数可以等待互斥对象的句柄,而WaitForMultipleExpressions函数不能等待互斥对象的句柄。如果将一个互斥对象的句柄传递给WaitForMultipleExpressions函数,那么WaitForMultipleExpressions函数将不能正确运行。因为WaitForMultipleExpressions函数内部使用了线程来等待每个表达式,而互斥对象可以被线程所拥有。一旦某个线程获得了对互斥对象的所有权,那么当线程终止运行时,它就会放弃该互斥对象。

关于WaitForMultipleExpressions函数的实现,有三个技巧在这里说明一下:
  • 利用信号量,使得多个线程只能有一个被唤醒。在生成线程之前,先创建一个信号量对象,其初始数量是1。然后,线程对WaitForMultipleObjectsEx的每次调用都包含该信号量句柄和表达式中的其他句柄,这也就是为什么每个表达式句柄最多可以包含63个句柄的原因。为了使一个线程醒来,它等待的所有对象,包括信号量对象,都必须处于有信号状态。由于信号量设定的初始数量是1,因此被唤醒的线程不能超过1个。之所以保证只能唤醒一个线程,是为了防止其他线程中的WaitForMultipleObjectsEx会改变它返回的那个内核对象的状态。
  • 利用异步过程调用(APC)队列,强制唤醒正在等待的其他线程。通过调用QueueUserAPC函数把一个APC对象加入到指定线程的APC队列中,这也就是为什么在线程中只能调用WaitForMultipleObjectsEx而不调用WaitForMultipleObjects的原因。
  • 利用信号量,巧妙地处理超时。如果线程在等待时间内没有任何表达式等待实现,那么主线程调用的WaitForMultipleObjects就返回一个WAIT_TIMEOUT值。如果出现这种情况,就要防止任何表达式得以实现。通过调用超时为0的WaitForSingleObject判断信号量的状态,如果它返回WAIT_OBJECT_0,说明主线程得到了信号量对象,并且没有一个表达式得以实现。如果返回WAIT_TIMEOUT,说明在主线程得到信号量对象前,一个表达式就已经得以实现。若要确定哪个表达式得到了实现,主线程要再次调用超时为INFINITE的WaitForMultipleObjects即可,因为已经知道肯定有一个线程得到了信号量。这时,必须强制全部线程醒来,这样它们才能彻底退出。

4.来自MSDN介绍


To wait on more than MAXIMUM_WAIT_OBJECTS handles, use one of the following methods:

  • Create a thread to wait on MAXIMUM_WAIT_OBJECTS handles, then wait on that thread plus the other handles. Use this technique to break the handles into groups of MAXIMUM_WAIT_OBJECTS.
  • Call RegisterWaitForSingleObject to wait on each handle. A wait thread from the thread pool waits on MAXIMUM_WAIT_OBJECTS registered objects and assigns a worker thread after the object is signaled or the time-out interval expires. 

关于第二种用线程池的方法,暂时没研究明白,还望哪个大神能够指点指点。这里只简单说一下第一种方法,即每64个句柄为一组,然后分别创建一个线程调用WaitForMultipleObjects等待一组句柄。其实,这种方法和Windows核心编程的WaitForMultipleExpressions函数的实现方式是异曲同工,所以我们可以参考借鉴WaitForMultipleExpressions函数的实现,实现具有以下功能的函数:


(1)在规定时间内,等待全部句柄处于有信号状态才返回。最多可以等待64*64=4096个句柄。实现如下:

#include<windows.h>
#include <malloc.h>
#include <process.h>
#include "WaitForMultExp.h"

#define MAX_KERNEL_OBJS     4096

///////////////////////////////////////////////////////////////////////////////
typedef struct
{
	DWORD nExpObjects;
	HANDLE *phExpObjects;
}EXPRESSION, *LPEXPRESSION;

unsigned __stdcall WFME_MultipleObjectsThread(void *lpParam)
{
	//wait for all of kernel objects in a expression to be signaled or a APC to be added to APC Queue in a thread
	LPEXPRESSION lpExpression = (LPEXPRESSION)lpParam;
	WaitForMultipleObjectsEx(lpExpression->nExpObjects, lpExpression->phExpObjects, true, INFINITE, true);
	return 0;
}

void CALLBACK WFME_MultipleObjectsAPC(ULONG_PTR dwParam)
{
	//only add it to a thread`s APC Queue to stop thread`s waiting
}

DWORD WINAPI WaitForMultipleObjectsExpand(DWORD nExpObjects, const HANDLE* phExpObjects, DWORD dwMilliSeconds)
{
	EXPRESSION Expressions[MAXIMUM_WAIT_OBJECTS];
	HANDLE *phExpObjectsTmp, ahThreads[MAXIMUM_WAIT_OBJECTS];
	//allocate the space on stack
	phExpObjectsTmp = (HANDLE*)_alloca(nExpObjects*sizeof(HANDLE));
	CopyMemory(phExpObjectsTmp, phExpObjects, nExpObjects*sizeof(HANDLE));

	DWORD dwWaitRet = 0;
	int nExpObjectsCount = nExpObjects;
	int NumExps = 0;
	int BeginPos = 0;
	int CurPos = 0;

	if (nExpObjects > MAX_KERNEL_OBJS)
	{
		dwWaitRet = WAIT_FAILED;
		SetLastError(ERROR_SECRET_TOO_LONG);
	}

	if (dwWaitRet != WAIT_FAILED)
	{
		// break the handles into groups of MAXIMUM_WAIT_OBJECTS
		while (nExpObjectsCount > MAXIMUM_WAIT_OBJECTS)
		{
			Expressions[NumExps].phExpObjects = &phExpObjectsTmp[BeginPos];
			Expressions[NumExps].nExpObjects = MAXIMUM_WAIT_OBJECTS;

			NumExps++;
			BeginPos += MAXIMUM_WAIT_OBJECTS;
			nExpObjectsCount -= MAXIMUM_WAIT_OBJECTS;
		}

		if (nExpObjectsCount > 0)
		{
			Expressions[NumExps].phExpObjects = &phExpObjectsTmp[BeginPos];
			Expressions[NumExps].nExpObjects = nExpObjectsCount;

			NumExps++;
		}

		// Create a thread to wait on MAXIMUM_WAIT_OBJECTS handles
		for (int i = 0; i < NumExps; i++)
			ahThreads[i] = (HANDLE)_beginthreadex(NULL, 0, WFME_MultipleObjectsThread, (void*)&Expressions[i], 0, NULL);

		dwWaitRet = WaitForMultipleObjects(NumExps, ahThreads, true, dwMilliSeconds);

		for (int i = 0; i < NumExps; i++)
		{
			if (dwWaitRet == WAIT_TIMEOUT)
			{
				QueueUserAPC(WFME_MultipleObjectsAPC, ahThreads[i], 0);
			}

			CloseHandle(ahThreads[i]);
		}
	}

	return dwWaitRet;
}


(2)在规定时间内,等待某一个句柄处于有信号状态就返回。同样最多可以等待64*64=4096个句柄。实现如下:

#include<windows.h>
#include <malloc.h>
#include <process.h>
#include "WaitForMultExp.h"

#define MAX_KERNEL_OBJS     4096

///////////////////////////////////////////////////////////////////////////////
typedef struct
{
	DWORD nExpObjects;
	HANDLE *phExpObjects;
	DWORD dwWaitRet;
}EXPRESSION, *LPEXPRESSION;

unsigned __stdcall WFME_MultipleObjectsThread(void *lpParam)
{
	//wait for all of kernel objects in a expression to be signaled or a APC to be added to APC Queue in a thread
	LPEXPRESSION lpExpression = (LPEXPRESSION)lpParam;
	
	DWORD dwWaitRet = WaitForMultipleObjectsEx(lpExpression->nExpObjects, lpExpression->phExpObjects, false, INFINITE, true);
	if (dwWaitRet != ERROR_EXE_MARKED_INVALID)
	{
		lpExpression->dwWaitRet = dwWaitRet;
	}
	return 0;
}

void CALLBACK WFME_MultipleObjectsAPC(ULONG_PTR dwParam)
{
	//only add it to a thread`s APC Queue to stop thread`s waiting
}

DWORD WINAPI WaitForMultipleObjectsExpand(DWORD nExpObjects, const HANDLE* phExpObjects, DWORD dwMilliSeconds)
{
	EXPRESSION Expressions[MAXIMUM_WAIT_OBJECTS];
	HANDLE *phExpObjectsTmp, ahThreads[MAXIMUM_WAIT_OBJECTS];
	//allocate the space on stack
	phExpObjectsTmp = (HANDLE*)_alloca(nExpObjects*sizeof(HANDLE));
	CopyMemory(phExpObjectsTmp, phExpObjects, nExpObjects*sizeof(HANDLE));

	DWORD dwWaitRet = 0;
	int nExpObjectsCount = nExpObjects;
	int NumExps = 0;
	int BeginPos = 0;
	int CurPos = 0;

	if (nExpObjects > MAX_KERNEL_OBJS)
	{
		dwWaitRet = WAIT_FAILED;
		SetLastError(ERROR_SECRET_TOO_LONG);
	}

	if (dwWaitRet != WAIT_FAILED)
	{
		// break the handles into groups of MAXIMUM_WAIT_OBJECTS
		while (nExpObjectsCount > MAXIMUM_WAIT_OBJECTS)
		{
			Expressions[NumExps].phExpObjects = &phExpObjectsTmp[BeginPos];
			Expressions[NumExps].nExpObjects = MAXIMUM_WAIT_OBJECTS;

			NumExps++;
			BeginPos += MAXIMUM_WAIT_OBJECTS;
			nExpObjectsCount -= MAXIMUM_WAIT_OBJECTS;
		}

		if (nExpObjectsCount > 0)
		{
			Expressions[NumExps].phExpObjects = &phExpObjectsTmp[BeginPos];
			Expressions[NumExps].nExpObjects = nExpObjectsCount;

			NumExps++;
		}

		// Create a thread to wait on MAXIMUM_WAIT_OBJECTS handles
		for (int i = 0; i < NumExps; i++)
			ahThreads[i] = (HANDLE)_beginthreadex(NULL, 0, WFME_MultipleObjectsThread, (void*)&Expressions[i], 0, NULL);

		dwWaitRet = WaitForMultipleObjects(NumExps, ahThreads, false, dwMilliSeconds);

		for (int i = 0; i < NumExps; i++)
		{
			if (dwWaitRet == WAIT_TIMEOUT || (dwWaitRet - WAIT_OBJECT_0) != i)
			{
				QueueUserAPC(WFME_MultipleObjectsAPC, ahThreads[i], 0);
			}

			CloseHandle(ahThreads[i]);
		}

		if (dwWaitRet != WAIT_TIMEOUT)
		{
			dwWaitRet = dwWaitRet*MAXIMUM_WAIT_OBJECTS + Expressions[dwWaitRet - WAIT_OBJECT_0].dwWaitRet;
		}
	}

	return dwWaitRet;
}
上面的这种实现方式其实是有问题的,现将问题说明一下:

  • 如果当等待的句柄数多于64个的时候,无法保证只有一个线程被唤醒。
  • 无法正确地处理超时,因为在主线程调用WaitForMultipleObjects和QueueUserAPC之间,线程可能被唤醒。

这里之所以要把有问题的代码贴出来,是因为当句柄数大于64个时,如果想实现等待到某一个句柄处于有信号状态就返回,在Windows平台下目前我还无法找到一个比较完美的实现方式,这也是写作本文的目的,如果有哪个网友实现了此功能或者有什么更好的想法,希望在看到此文时能够不吝赐教。


你可能感兴趣的:(探讨WaitForMultipleObjects如何突破64个句柄的限制)