c++multithreadingc++11mutex

C++11: Why does std::condition_variable use std::unique_lock?


I am a bit confused about the role of std::unique_lock when working with std::condition_variable. As far as I understood the documentation, std::unique_lock is basically a bloated lock guard, with the possibility to swap the state between two locks.

I've so far used pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) for this purpose (I guess that's what the STL uses on POSIX). It takes a mutex, not a lock.

What's the difference here? Is the fact that std::condition_variable deals with std::unique_lock an optimization? If so, how exactly is it faster?


Solution

  • so there is no technical reason?

    I upvoted cmeerw's answer because I believe he gave a technical reason. Let's walk through it. Let's pretend that the committee had decided to have condition_variable wait on a mutex. Here is code using that design:

    void foo()
    {
        mut.lock();
        // mut locked by this thread here
        while (not_ready)
            cv.wait(mut);
        // mut locked by this thread here
        mut.unlock();
    }
    

    This is exactly how one shouldn't use a condition_variable. In the regions marked with:

    // mut locked by this thread here
    

    there is an exception safety problem, and it is a serious one. If an exception is thrown in these areas (or by cv.wait itself), the locked state of the mutex is leaked unless a try/catch is also put in somewhere to catch the exception and unlock it. But that's just more code you're asking the programmer to write.

    Let's say that the programmer knows how to write exception safe code, and knows to use unique_lock to achieve it. Now the code looks like this:

    void foo()
    {
        unique_lock<mutex> lk(mut);
        // mut locked by this thread here
        while (not_ready)
            cv.wait(*lk.mutex());
        // mut locked by this thread here
    }
    

    This is much better, but it is still not a great situation. The condition_variable interface is making the programmer go out of his way to get things to work. There is a possible null pointer dereference if lk accidentally does not reference a mutex. And there is no way for condition_variable::wait to check that this thread does own the lock on mut.

    Oh, just remembered, there is also the danger that the programmer may choose the wrong unique_lock member function to expose the mutex. *lk.release() would be disastrous here.

    Now let's look at how the code is written with the actual condition_variable API that takes a unique_lock<mutex>:

    void foo()
    {
        unique_lock<mutex> lk(mut);
        // mut locked by this thread here
        while (not_ready)
            cv.wait(lk);
        // mut locked by this thread here
    }
    
    1. This code is as simple as it can get.
    2. It is exception safe.
    3. The wait function can check lk.owns_lock() and throw an exception if it is false.

    These are technical reasons that drove the API design of condition_variable.

    Additionally, condition_variable::wait doesn't take a lock_guard<mutex> because lock_guard<mutex> is how you say: I own the lock on this mutex until lock_guard<mutex> destructs. But when you call condition_variable::wait, you implicitly release the lock on the mutex. So that action is inconsistent with the lock_guard use case / statement.

    We needed unique_lock anyway so that one could return locks from functions, put them into containers, and lock/unlock mutexes in non-scoped patterns in an exception safe way, so unique_lock was the natural choice for condition_variable::wait.

    Update

    bamboon suggested in the comments below that I contrast condition_variable_any, so here goes:

    Question: Why isn't condition_variable::wait templated so that I can pass any Lockable type to it?

    Answer:

    That is really cool functionality to have. For example this paper demonstrates code that waits on a shared_lock (rwlock) in shared mode on a condition variable (something unheard of in the posix world, but very useful nonetheless). However the functionality is more expensive.

    So the committee introduced a new type with this functionality:

    condition_variable_any
    

    With this condition_variable adaptor one can wait on any lockable type. If it has members lock() and unlock(), you are good to go. A proper implementation of condition_variable_any requires a condition_variable data member and a shared_ptr<mutex> data member.

    Because this new functionality is more expensive than your basic condition_variable::wait, and because condition_variable is such a low level tool, this very useful but more expensive functionality was put into a separate class so that you only pay for it if you use it.