Search This Blog

Friday, June 10, 2005

Events - New POSIX Implementation

The new POSIX version of the event is similar to, but a bit more complicated than the fast semaphore. In fact, the POSIX version of the standard event is inherited from a platform-independent fast event. It's based on the same principle as the fast semaphore: use of an atomic variable in combination with a slow semaphore than never increases above 0. A bit more complicated, though, because now our atomic variable is a compound value, rather than a single counter.

The reason for this is that we can only operate on a single variable atomically, yet we have two values that must be tracked - the status of the event (set or cleared), and the number of waiters (if cleared). The compound value structure consists of the highest bit indicating that the event is set, and the low bits indicating the number of waiters. Logically, it's impossible for there to be any waiters when the event is set.

With this configuration, it should be fairly obvious how to implement this. The implementation will rely heavily on the fact that we can read a variable and then change it at a later time, as long as we can verify that the variable hasn't been changed in between (which we can, using AtomicCompareExchange - if the variable has been changed when we try to write it back, we simply read it again and repeat). This allows us to read the status value, perform logic on it, and then write it back with some computed new value, and still be totally thread-safe in the whole operation.

The Reset and TryWait methods are trivial - a single atomic function call - so I won't bother explaining them. Set and Wait aren't much more complicated, but I'll mention them a bit.

The only real logic that Set requires is what to do if the event is cleared. If it's a manual-reset event, all waiting threads get released, and the status becomes set. If it's an auto-reset event, what happens depends on whether there are waiting threads. If there are, then one thread is released, and the status remains cleared (although there's one less waiting thread). If no threads are waiting, the status becomes set, just like with the manual-reset event.

The logic in Wait is essentially the same. If the status isn't set, then the waiting thread count gets implemented, and the thread waits on the semaphore. If it's set and it's a manual-reset event, Wait returns immediately, and the status remains set. If the status is set and it's an auto-reset event, the status is cleared, and Wait returns (at this point no threads are waiting on the event).

The code for CFastEvent and the POSIX CEvent:

class CFastEvent
{
private:
CSemaphore m_sem; // Semaphore that threads will wait on

unsigned int m_nSpinCount; // Constant. The number of times to attempt to check if the event is set before waiting

bool m_bAutoReset; // Constant

int32 m_nStatus; // Atomically accessed. The sign bit indicates that the event is currently set. The rest of the bits are the number of waiting threads on the event when the event isn't set.

// Various local constants
enum
{
WAITER_MASK = 0x7FFFFFFFL,
SET = 0x80000000L
};

public:

inline CFastEvent(bool bAutoReset, bool bSet, unsigned int nSpinCount = 0) : m_sem(0)
{
assert(nSpinCount >= 0);

m_nSpinCount = nSpinCount;

m_bAutoReset = bAutoReset;

m_nStatus = (bSet ? SET : 0);
}

inline ~CFastEvent()
{
}

void Set()
{
int32 nStatus, nNewStatus;
do
{
nStatus = m_nStatus;

if (nStatus == SET)
return; // Already set

if (nStatus && m_bAutoReset)
nNewStatus = nStatus - 1; // If there are waiters and it's an auto-reset event, release 1 and leave unset
else
nNewStatus = SET; // Else set and release all
} while (AtomicCompareExchange(&m_nStatus, nNewStatus, nStatus) != nStatus);

// If there were any waiters, release the appropriate number
if (nStatus)
m_sem.Post(nStatus - (nNewStatus & WAITER_MASK));
}

inline void Reset()
{
// If it's set, reset it; else do nothing
AtomicCompareExchange(&m_nStatus, 0, SET);
}

inline bool TryWait()
{
// New event status if we successfully get the event
int32 nNewStatus = (m_bAutoReset ? 0 : SET);

// If it's previously set, we got it. Set or reset it based on whether it's an auto or manual-reset event.
return (AtomicCompareExchange(&m_nStatus, nNewStatus, SET) == SET);
}

// Wait indefinitely for the event
void Wait()
{
// Don't wait if we can get it by spinning
unsigned int nSpinCount = m_nSpinCount;
while (nSpinCount--)
{
if (TryWait())
return;
}

// Check if the event is set, queue as a waiter if not
int32 nStatus, nNewStatus;
do
{
nStatus = m_nStatus;

if (nStatus == SET)
nNewStatus = (m_bAutoReset ? 0 : SET); // Set or reset it
else
nNewStatus = nStatus + 1; // One more waiter
} while (AtomicCompareExchange(&m_nStatus, nNewStatus, nStatus) != nStatus);

if (nStatus == SET)
return; // It was set

// It wasn't set. Wait for it.
m_sem.Wait();
}
};

// Simply provide the CFastEvent class with an interface compatible with CEvent
class CEvent : public CFastEvent
{
public:
inline CFastEvent(bool bAutoReset, bool bSet) : CFastEvent(bAutoReset, bSet)
{ }
};

No comments: