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.