Search This Blog

Friday, May 27, 2005

Events - POSIX Version

POSIX, on the other hand, does not support events; rather, it supports something called condition variables. Condition variables are a way of signalling waiting thread(s) that a condition inside a mutex has become true (generally when a variable takes a certain value). You do this by calling pthread_cond_signal to signal one waiting thread, and pthread_cond_broadcast to signal all waiting threads. However, conditions make you do a bit more yourself. You are expected to check that the value of the variable isn't what you want (inside the mutex) before trying to wait on the condition, and signalling a condition that no threads are waiting on has no effect whatsoever.

What's cool about conditions, though, is that they are linked to a variable in a mutex. To check if the condition is true, you'd do the following:
1. Enter the mutex
2. Check the value of the variable to see that the condition isn't already true
3. Wait on the condition to be signalled
4. Verify that the variable is now set so that the condition is true
5. Do whatever you want when the condition is true
6. Unlock the mutex

If you're a Windows programmer and you're used to using a separate mutex and event for this kind of thing, you'll notice a conspicuous absence: the mutex is not left before waiting on the condition, nor is it entered after waiting. You'd quickly learn of the error of your ways if you tried this with an event and mutex, as you'd deadlock the thread. The mutex doesn't remain locked while the thread is waiting on the condition, but it relocked when the thread comes out of wait.

Anyway, I'm getting sidetracked. So, here's the game plan (not that it's anything remarkable): a bool variable protected by a mutex, and a condition for threads to wait on. Threads waiting on the event will first check the bool, and if not set, wait on the condition. Threads setting the event will check if the event is false, and if so, set it and signal as many threads are appropriate (1 for auto-reset events, all for manual-reset events). As an optimization to prevent the signal if no threads are waiting, we tac on a waiting threads counter. And of course some crap to convert the relative time to wake to the absolute time to wake. The code:

class CEvent // POSIX version of event: condition variable (fast?)
{
private:
bool m_bAutoReset; // Constant

pthread_mutex_t m_mutex; // Mutex
pthread_cond_t m_cond; // Condition variable

// Protected by m_mutex
bool m_bSet; // Set or clear

unsigned int m_nWaitingThreads; // Number of threads waiting on the event

// As the name implies, this must be called inside the mutex
// Does the wait. The parameter specifies when the thread should wake up, should the event not get set before then. If this is NULL, the thread will wait indefinitely on the event. Returns whether the event got set (if not, the timeout must have expired).
bool InnerWait(const timespec *restrict abstime)
{
if (!m_bSet)
{
m_nWaitingThreads++;

do
{
int nRetVal;

// Do the wait, either timed or indefinite
if (abstime)
nRetVal = pthread_cond_timedwait(&m_cond, &m_mutex, abstime);
else
nRetVal = pthread_cond_wait(&m_cond, &m_mutex);

assert(nRetVal == 0 || nRetVal == ETIMEDOUT);
} while (!m_bSet && nRetVal != ETIMEDOUT); // Loop until it gets set or the timeout expires

m_nWaitingThreads--;
}

// Did the event get set?
bool bSuccess = m_bSet;
// If the event is set and it's an auto-reset event, reset it now that we're awake
if (m_bSet && m_bAutoReset)
m_bSet = false;

return bSuccess;
}

public:
CEvent(bool bAutoReset, bool bSet)
{
if (pthread_mutex_init(&m_mutex, NULL) != 0)
throw std::bad_alloc("Unable to create mutex");
else if (pthread_cond_init(&m_cond, NULL) != 0)
{
pthread_mutex_destroy(&m_mutex);

throw std::bad_alloc("Unable to create condition variable");
}

m_bAutoReset = bAutoReset;
m_bSet = bSet;

m_nWaitingThreads = 0;
}

inline ~CEvent()
{
pthread_cond_destroy(&m_cond);
pthread_mutex_destroy(&m_mutex);
}

void Set()
{
pthread_mutex_lock(&m_mutex);

if (!m_bSet) // If it's already set, do nothing
{
m_bSet = true; // Set the event

// Check if there are any waiters, and release them appropriately
if (m_nWaitingThreads)
{
if (m_bAutoReset)
pthread_cond_signal(&m_cond); // Release one thread
else
pthread_cond_broadcast(&m_cond); // Release all threads
}
}

pthread_mutex_unlock(&m_mutex);
}

inline void Reset()
{
pthread_mutex_lock(&m_mutex);

m_bSet = false; // Ding

pthread_mutex_unlock(&m_mutex);
}

bool Wait(unsigned int nTimeoutMS)
{
// Calculate the time to wake based on the time to sleep. I hope I understand how this is supposed to work on POSIX.
timespec now, timeout, later;

clock_gettime(CLOCK_REALTIME, &now);

timeout.tv_sec = nTimeoutMS / 1000; // Seconds
timeout.tv_nsec = (nTimeoutMS % 1000) * 1000000L; // Nanoseconds

later.tv_sec = now.tv_sec + timeout.tv_sec;
later.tv_nsec = now.tv_nsec + timeout.tv_nsec;
if (later.tv_nsec >= 1000000000L)
{
later.tv_nsec -= 1000000000L;
later.tv_sec++;
}

pthread_mutex_lock(&m_mutex);

bool bSuccess = InnerWait(&later);

pthread_mutex_unlock(&m_mutex);

return bSuccess;
}

inline void Wait()
{
pthread_mutex_lock(&m_mutex);

InnerWait(NULL);

pthread_mutex_unlock(&m_mutex);
}
};

1 comment:

Demofox said...

Awesome, thanks for this!

I'm about to use it in a google NaCl application, thank you very much!

A code sample of how to use the class would be a cherry on top.