Easier Windows Event Multiplexing on Waiting for Multiple Objects

WaitForMultipleObjects returns a strangely cooked DWORD value that can have flags meaning error, time out, signaled or abandoned condition, as well as an index into a handle in the array that is passed to it. It is even worse, if you change the number of the event handles over time, because the returned index can change too even the event is signaled from the same handle.

In the latest May 2011 issue of MSDN Magazine, Alex Gimenez tries to create a wrapper class to mitigate the problem in his articleDynWaitList: ID-Based Windows Event Multiplexing. When a handle is added to a DynWaitList object, the client gets back a unique ID for the handle for the life of the DynWaitList object. When the Wait call returns, the client gets an ID instead of variably meaningless index for the handle that signaled. So the client is confident that the ID would exactly mean the corresponding event, even the actual handles in the DynWaitList object can change over time.

The solution is perfectly fine for the author’s purpose. But when I try to use it, I get a problem. Now for each event, I have two pieces of information that I need to maintain, the handle, and the ID. Aren’t they redundant? For example, if I know idEventBusArrive has signaled, I may still need to access hEventBusArrive, for example, to reset it. Isn’t it better if waiting returns directly hEventBusArrive for me?

Let’s have a further look at the return value of WaitForMultipleObjects. Obviously, the Windows API designer tries to pack at least two pieces of information in the 32-bit DWORD: waiting status (signaled, abandoned, timed out, or failed); and information of the actual handle in the handle array if relevant. Because a handle of type HANDLE (in fact, void*) takes 32-bit on Win32 and 64-bit on Win64, the designer was forced to used an index instead, which has a very limited range, 0 to 63 (Windows allows waiting for at most 64 handles in WaitForMultipleObjects).

In my point view, if WaitForMultipleObjects could return the offending event handle, it would be perfect. The function could have taken an extra pointer arguments, HANDL* pHandle to return the exact handle that either signaled or abandoned, and let the original return value DWORD to only indicate the waiting status. No index or ID required. What kind of ID would be better than the handle? None, because the HANDLE is simply the unique ID for the event.

With this idea in mind, it is then simple and easy to write my wrapper class for WaitForMultipleObjects. In fact, I would wrap up WaitForMultipleObjectsEx since it is a super set of WaitForMultipleObjects. Here is the class, MultipleObjectsWaiter (maybe MultipleObjectsWaitress is a better name):

  1 #include <windows.h>
  2 #include <vector>
  3 #include <algorithm>
  4 #include <utility>
  5 #include <cassert>
  6 
  7 class MultipleObjectsWaiter
  8 {
  9 public:
 10 	enum Status
 11 	{
 12 		Signaled = WAIT_OBJECT_0,
 13 		Abandoned = WAIT_ABANDONED_0,
 14 		TimedOut = WAIT_TIMEOUT,
 15 		Alerted = WAIT_IO_COMPLETION,
 16 		Failed = WAIT_FAILED,
 17 	};
 18 
 19 	typedef std::pair<Status, HANDLE> Result;
 20 
 21 public:
 22 	// if you know max handle count to be added, give the hint as a parameter
 23 	MultipleObjectsWaiter(size_t hintObjects = 0)
 24 	{
 25 		m_handles.reserve(hintObjects);
 26 	}
 27 
 28 	// add given handles if any.
 29 	MultipleObjectsWaiter(const HANDLE* handles, size_t count, size_t hintObjects = 0)
 30 	{
 31 		m_handles.reserve( hintObjects>count? hintObjects : count );
 32 		assert( count<=0 || handles );
 33 		for(size_t i=0; i<count; ++i)
 34 			Add( handles[i] );
 35 	}
 36 
 37 	// add handle to waiting list. 
 38 	// return false if handle already there or there are too many handles (array full).
 39 	bool Add(HANDLE handle)
 40 	{
 41 		assert(handle);
 42 		if( m_handles.end() != std::find(m_handles.begin(), m_handles.end(), handle) )
 43 			return false;	// handle already there.
 44 		if( m_handles.size() >= MAXIMUM_WAIT_OBJECTS ) // Windows limitation
 45 			return false;	// handle array full
 46 		m_handles.push_back(handle);
 47 		return true;
 48 	}
 49 
 50 	// remove a handle from the waiting list
 51 	// return false if handle not found
 52 	bool Remove(HANDLE handle)
 53 	{
 54 		assert(handle);
 55 		std::vector<HANDLE>::iterator it = std::find(m_handles.begin(), m_handles.end(), handle);
 56 		if( m_handles.end() == it )
 57 			return false;
 58 		m_handles.erase(it);
 59 		return true;
 60 	}
 61 
 62 	// Possible waiting conditions:
 63 	//   waiting for all 2 or more handles : bWaitAll == TRUE  and handle count >= 2;
 64 	//   waiting for at most 1 handle      : bWaitAll == FALSE or  handle count == 1;
 65 	// Possible return values:
 66 	//   (Signaled, handle) : the handle signaled, if waiting for at most 1 handle.
 67 	//   (Signaled, NULL)   : all handles signaled, if waiting for 2 or more handles.
 68 	//   (Abandoned,handle) : the handle abandoned ,if waiting for at most 1 handle.
 69 	//   (Abandoned,NULL)   : at least one handle abandoned, if waiting for 2 or more handles.
 70 	//   (TimedOut, NULL)   : timed out, any waiting condition.
 71 	//   (Alerted,  NULL)   : wait ended early due to I/O completion or APC, may happen only when bAlertable == TRUE.
 72 	//   (Failed,   NULL)   : wait failed, any waiting condition, or no handle to wait.
 73 	Result Wait(DWORD dwTimeoutMs = INFINITE, BOOL bWaitAll = FALSE, BOOL bAlertable = FALSE)
 74 	{
 75 		if( m_handles.size() > 0 )
 76 		{
 77 			DWORD rc = ::WaitForMultipleObjectsEx((DWORD)m_handles.size(), &m_handles.front(), bWaitAll, dwTimeoutMs, bAlertable);
 78 			if( rc >= WAIT_OBJECT_0 && rc < WAIT_OBJECT_0+m_handles.size() )
 79 				return Compose(Signaled, bWaitAll? NULL : m_handles[rc-WAIT_OBJECT_0]);
 80 			else if( rc >= WAIT_ABANDONED_0 && rc < WAIT_ABANDONED_0+m_handles.size() )
 81 				return Compose(Signaled, bWaitAll? NULL : m_handles[rc-WAIT_OBJECT_0]);
 82 			else if( rc == WAIT_TIMEOUT )
 83 				return Compose(TimedOut, NULL);
 84 			else if( rc == WAIT_IO_COMPLETION )
 85 				return Compose(Alerted, NULL);
 86 			else
 87 			{
 88 				assert( rc == WAIT_FAILED );
 89 				return Compose(Failed, NULL);
 90 			}
 91 		}
 92 		assert(false);	// no handles.
 93 		return Compose(Failed, NULL); // time critical, so return error instead of throw exception
 94 	}
 95 
 96 	// If you have to see all the handles...
 97 	const std::vector<HANDLE>& Handles() const { return m_handles; }
 98 
 99 private:
100 	Result Compose(Status s, HANDLE h) { return Result(s, h); }
101 private:
102 	std::vector<HANDLE>		m_handles;
103 };

The most important function is Wait. It returns in its results, a pair of Status and HANDLE, the exact two pieces of information we want from WaitForMultipleObjectsEx. The rest of the class is trivial, basically allowing you to add and remove handles from the waiting array.

A simple usage is like this:

  1 void foo()
  2 {
  3 	HANDLE hEventBusArrive  = ::CreateEvent(...);
  4 	HANDLE hEventTaxiArrive = ::CreateEvent(...);
  5 
  6 	MultipleObjectsWaiter waiter(2);
  7 	waiter.Add(hEventBusArrive);
  8 	waiter.Add(hEventTaxiArrive);
  9 	
 10 	// ...
 11 	BOOL bWaitAll = ...; 
 12 	DWORD dwTimeOutMs = ...;
 13 	BOOL bAlertable = ...;
 14 
 15 	MultipleObjectsWaiter::Result r = waiter.Wait(dwTimeOutMs, bWaitAll, bAlertable);
 16 	switch( r.first )
 17 	{
 18 	case MultipleObjectsWaiter::Signaled:
 19 		if( r.second==NULL )
 20 			; // both bus and taxi arrived.
 21 		else if( r.second==hEventBusArrive )
 22 			; // bus arrived
 23 		else if( r.second==hEventTaxiArrive )
 24 			; // taxi arrived
 25 		else
 26 			; // should not be here!
 27 		break;
 28 	case MultipleObjectsWaiter::Abandoned:
 29 		if( r.second==NULL )
 30 			; // at least 1 handle abandoned (wait for all)
 31 		else if( r.second==hEventBusArrive )
 32 			; // bus arrive event abandoned (not wait for all)
 33 		else if( r.second==hEventTaxiArrive )
 34 			; // taxi arrive event abandoned (not wait for all)
 35 		else
 36 			; // should not be here
 37 		break;
 38 	case MultipleObjectsWaiter::TimedOut: // timed out
 39 		break;
 40 	case MultipleObjectsWaiter::Alerted:  // alerted
 41 		break;
 42 	case MultipleObjectsWaiter::Failed:   // error
 43 		break;
 44 	default:
 45 		assert(false);
 46 	}
 47 }

Notice that everything about an event is the event handle, which is the only information used in processing the wait result. As long as the event handles such as hEventBusArrive and hEventTaxiArrive are up-to-date, which they should, the processing logic does not need to change even if you dynamically Add to or Remove from MultipleObjectsWaiter the handles.

In summary, MultipleObjectsWaiter does what WaitForMultipleObjects and WaitForMultipleObjectsEx should do: return the waiting status and the relevant object handle out of the handle array if applicable. It is therefore simpler, faster, and easier to use than DynWaitList.

你可能感兴趣的:(Easier Windows Event Multiplexing on Waiting for Multiple Objects)