Monday, April 7, 2014

In exceptional conditions

On the topic of condition variables, wikipedia says: “In concurrent programming, a monitor is a synchronization construct that allows threads to have both mutual exclusion and the ability to wait (block) for a certain condition to become true.”
This is commonly used in producer/consumer-like problems.

The code for programming such a monitor looks like this:
lock(&mutex);
while (!predicate) {
    condition_wait(&mutex);
}
consume(&item);
unlock(&mutex);
This can be implemented, for example, with pthread_mutex_lock() and pthread_cond_wait(). If you would wrap things into a class, it might look like the following:
mutex.lock();
while (!predicate) {
    cond.wait(mutex);
}
consume(item);
mutex.unlock();
However, in a language that uses exceptions, this code would be flawed. Suppose that consume() (or even cond.wait(), however unlikely) throws an exception, then the mutex remains locked. Therefore, in a language like Java, you should write:
mutex.lock();
try {
    while (!predicate) {
        cond.wait(mutex);
    }
    consume(item);
} finally {
    mutex.unlock();
}
This ensures the mutex gets unlocked, even in case an exception occurs.
In Python, the mutex is part of the condition object. This simplifies things somewhat by hiding the mutex under the hood:
cond.acquire()
try:
    while not predicate:
        cond.wait()
    consume(item)
finally:
    cond.release()
C++ does not have a finally keyword — it doesn't need it, because it has RAII (“Resource Acquisition Is Initialisation”). In C++, it should be coded as:
std::mutex mx;
{
    std::unique_lock<std::mutex> lk(mx);
    while (!predicate) {
        cond.wait(lk);
    }
    consume(item);
}
Note that the destructor of std::unique_lock will ensure that the mutex is unlocked. The destructor will run when the stack unwinds, which is also done when an exception occurs.

Although entirely correct, and arguably the most elegant solution, I have to say I find the C++ code unintuitive and difficult to follow. The reason is that there are no explicit statements in this code block for dealing with locking; the mutex is locked by the constructor and unlocked by the destructor, that run automagically. It is almost as if the mutex variable isn't doing anything at all. Annoyingly, this code can not be refactored to resemble the other code fragments, because of RAII. And you are being forced to write it this way now that threads, mutexes, and condition variables are part of the std library; there is no other way in which a condition variable can be used because the other way would be incorrect in C++.
This is one of those cases where C++ shines but somehow it doesn't feel gratifying.