Search This Blog

Monday, May 23, 2005

Semaphore - Part 2

Okay, so now that we know what a semaphore is, how do we implement it? Well, pretty uninterestingly, actually. As mentioned previously, both Windows and POSIX have semaphore APIs right out of the box (well, POSIX with the semaphores extension). So this won't be much work at all.

First of all, let me explain one design decision common to both the Windows and the POSIX implementations. The semaphore class does not allow the use of named (inter-process) semaphores, even though both Windows and POSIX supports them. The reason for this is that, for the moment, the LibQ functions are intended to be process-local. While many classes are designed to work with thread synchronization, they are not designed for inter-process synchronization. I might go back later and add some classes for inter-process communication, but not now.

Now, Windows implements semaphores as kernel objects (HANDLEs). We create the semaphore with CreateSemaphore, destroy it with CloseHandle, increment it with ReleaseSemaphore, and wait on it with the ubiquitous WaitForSingleObject.

Looking at the documentation for those functions, you can see that I left one thing out of the semaphore class: a maximum value limit. The reason for this is that, to my knowledge, the POSIX semaphore API doesn't support this. Since LibQ is supposed to function identically on Windows and POSIX, that means we can't use it.

You'll also notice that I use WaitForSingleObject, rather than an alertable call to WaitForSingleObjectEx. The reason for this is that I thought it was best to not deal with the complications of alertable waits, since we'd just have to hide it from the application anyway, and we may or may not be able to guarantee similar behavior on POSIX. There is actually another reason for this, which is due to planning for asynchronous file access, but that's getting a bit ahead of the blog. We'll come back to that.

Looking at the class itself, you'll notice there's a private GetSemaphore function which returns the HANDLE. The reason this is private is that this is something programs should NOT be using, since it's platform dependant. The reason it exists at all is so that it can be used in other LibQ classes internally in the Windows version (in some places this is necessary).

You'll notice that I use inlining profusely. I intended LibQ classes to be as lightweight as possible. Given how short these functions all are, inlining was the natural choice. In fact, a sizeable amount of functions in all of the LibQ classes are inline, to avoid the overhead of a function call.

Lastly, you can see that I use bad_alloc to indicate the inability to create the semaphore. I chose to use this because it's convenient, and there's no need to go creating exception classes for every error that could possibly occur in the constructors of any of these classes. And besides that, bad_alloc does fit the description, roughly.

Without further ado, the Windows version of CSemaphore (sorry, I'm not familiar with posting code online; it lost all my indentations):



class CSemaphore // Win32 version of CSemaphore (slow)
{
private:
HANDLE m_hSem; // The semaphore

// Internal function to be used by LibQ itself
inline HANDLE GetSemaphore()
{ return m_hSem; }

public:
// nInitialCount is the initial semaphore count. Must be nonnegative
inline CSemaphore(long nInitialCount)
{
assert(nInitialCount >= 0);

m_hSem = CreateSemaphore(NULL, nInitialCount, LONG_MAX, NULL);
if (!m_hSem)
throw std::bad_alloc("Unable to create semaphore");
}

inline ~CSemaphore()
{
verify(CloseHandle(m_hSem));
}

// Try to get the semaphore without waiting. Fails if can't.
inline bool TryWait(unsigned long nTimeoutMS)
{
DWORD nRetVal = WaitForSingleObject(m_hSem, 0);
assert(nRetVal == WAIT_OBJECT_0 || nRetVal == WAIT_TIMEOUT);

if (nRetVal == WAIT_TIMEOUT)
return false;

return true;
}

// Waits indefinitely for the semaphore
inline void Wait()
{
verify(WaitForSingleObject(m_hSem, INFINITE) == WAIT_OBJECT_0);
}

// Increases the semaphore's count by nCount. Usually the default of 1 is desired. This count must be positive.
inline void Post(long nCount = 1)
{
assert(nCount > 0);

verify(ReleaseSemaphore(m_hSem, nCount, NULL));
}
};


UPDATE: I've been thinking it over, and I've decided to remove the timed wait function. The reason for this is due to the fact that I've decided to change the POSIX version of CSemaphore to use XSI semaphores, rather than the semaphore option semaphores; XSI semaphores do not support timed waits. The reason for this change is that semaphore option semaphores don't support posting more than one at a time. Because each post requires a transition to kernel mode, this could be a serious performance issue if it's common to release many threads at once (which I think will be more important than timed waits). So now I need to rewrite the POSIX version of CSemaphore :P

Also, I added the macro verify(x). This is similar to assert, but in non-debug build it gets substituted by (x) so that it will still be evaluated. Will be using that lots in the future.

No comments: