C++11 std::condition_variable: can we pass our lock directly to the notified thread?
Asked Answered
L

4

16

I'm learning about C++11 concurrency, where my only prior experience with concurrency primitives was in Operating Systems class six years ago, so be gentle, if you can.

In C++11, we can write

std::mutex m;
std::condition_variable cv;
std::queue<int> q;

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    cv.notify_one();
}

void consumer_thread() {
    std::unique_lock<std::mutex> lock(m);
    while (q.empty()) {
        cv.wait(lock);
    }
    q.pop();
}

This works fine, but I'm offended by the need to wrap cv.wait in a loop. The reason we need the loop is clear to me:

Consumer (inside wait())       Producer            Vulture

release the lock
sleep until notified
                               acquire the lock
                               I MADE YOU A COOKIE
                               notify Consumer
                               release the lock
                                                   acquire the lock
                                                   NOM NOM NOM
                                                   release the lock
acquire the lock
return from wait()
HEY WHERE'S MY COOKIE                              I EATED IT

Now, I believe one of the cool things about unique_lock is that we can pass it around, right? So it would be really elegant if we could do this instead:

Consumer (inside wait())       Producer

release the lock
sleep until notified
                               acquire the lock
                               I MADE YOU A COOKIE
                               notify and yield(passing the lock)
wake(receiving the lock)
return from wait()
YUM
release the lock

Now there's no way for the Vulture thread to swoop in, because the mutex remains locked all the way from I MADE YOU A COOKIE to YUM. Plus, if notify() requires that you pass a lock, that's a good way to ensure that people actually lock the mutex before calling notify() (see Signalling a condition variable (pthreads)).

I'm pretty sure that C++11 doesn't have any standard implementation of this idiom. What's the historical reason for that (is it just that pthreads didn't do it? and then why is that)? Is there a technical reason that an adventurous C++ coder couldn't implement this idiom in standard C++11, calling it perhaps my_better_condition_variable?

I also have a vague feeling that maybe I'm reinventing semaphores, but I don't remember enough from school to know if that's accurate or not.

Lowson answered 15/6, 2012 at 0:8 Comment(10)
Erm, if you don't want some thread to eat the cookie, why are you even running it?Ferrigno
Good point, the tone of my question was a bit biased against Vultures. :) See, Consumer is having a nice dream. He's willing to be woken up for cookies and milk, but if he wakes up and finds no cookies, he's going to be upset. So it's okay for Producer to feed cookies to Vulture, but it's not okay for Producer to wake up Consumer by shaking him and yelling "COOKIE TIME!" and then going "oh, whoops, Vulture eated your cookie while I was shaking you." That's not cool, Producer. That's not cool.Lowson
That makes no sense. You can't expect multiple unrelated threads that are unaware of the other threads accessing the same shared data to work correctly. How would Vulture ever eat any cookies with your proposed solution?Ferrigno
Oh come on. Slow down and read. (Is there an instant-message-type thing we could take this to, to avoid cluttering these comments?) Vulture can eat as many cookies as he likes, by waiting on the same condition variable as Consumer. In fact, the situation is perfectly symmetric; perhaps Vulture would prefer not to be woken up just to find out that Consumer had eaten his cookie.Lowson
Furthermore, Vulture can eat as many cookies as he likes simply by acquiring the mutex, anytime he likes. The only rule is that Vulture can't acquire the mutex while Producer is still holding it (during the period that Producer is yelling "COOKIE TIME!" at Consumer). I could write up an implementation of what I have in mind, if you like...Lowson
There's a chat feature. You're welcome to join us in the Lounge<C++>.Ferrigno
Oh WTF. "You need 20 reputation to talk here." :P Can someone please give me a bunch of reputation?Lowson
let us continue this discussion in chatLowson
+1 For lovely story about vultures :).Ping
In 'producer_thread()' you call 'notify_one()' while still holding the lock. Couldn't that be an issue?Lyte
N
10

The ultimate answer is because pthreads didn't do it. C++ is a language that encapsulates operating system functionality. C++ is not an operating system or platform. And so it encapsulates the existing functionality of operating systems such as linux, unix and windows.

However pthreads also has a good rationale for this behavior as well. From the Open Group Base Specifications:

The effect is that more than one thread can return from its call to pthread_cond_wait() or pthread_cond_timedwait() as a result of one call to pthread_cond_signal(). This effect is called "spurious wakeup". Note that the situation is self-correcting in that the number of threads that are so awakened is finite; for example, the next thread to call pthread_cond_wait() after the sequence of events above blocks.

While this problem could be resolved, the loss of efficiency for a fringe condition that occurs only rarely is unacceptable, especially given that one has to check the predicate associated with a condition variable anyway. Correcting this problem would unnecessarily reduce the degree of concurrency in this basic building block for all higher-level synchronization operations.

An added benefit of allowing spurious wakeups is that applications are forced to code a predicate-testing-loop around the condition wait. This also makes the application tolerate superfluous condition broadcasts or signals on the same condition variable that may be coded in some other part of the application. The resulting applications are thus more robust. Therefore, IEEE Std 1003.1-2001 explicitly documents that spurious wakeups may occur.

So basically the claim is that you can build my_better_condition_variable on top of a pthreads condition variable (or std::condition_variable) fairly easily and without performance penalty. However if we put my_better_condition_variable at the base level, then those clients who did not need the functionality of my_better_condition_variable would have to pay for it anyway.

This philosophy of putting the fastest, most primitive design at the bottom of the stack, with the intent that better/slower things can be built on top of them, runs throughout the C++ lib. And where the C++ lib fails to follow this philosophy, clients are often (and rightly) irritated.

Nomination answered 15/6, 2012 at 0:35 Comment(4)
R. Martinho Fernandes and I had a nice long discussion in chat, so I don't have many more unanswered questions. pthreads made the decision that you can't lock a mutex in one thread and unlock it in another; therefore it's impossible to pass a lock on a pthreads mutex from Producer to Consumer in my example (and std::mutex is modeled on pthreads). Building my_better_condition_variable would therefore require more work than I thought, because you'd first have to build my_unsafer_mutex. It'd be a whole threads library parallel to and incompatible with the C++11 library.Lowson
"So basically the claim is that you can build ... fairly easily and without performance penalty." And then "However if we put ... would have to pay for it anyway." If there's no performance penalty, then what are they paying for exactly?Interjoin
@ildjarn: As explained in the posix rationale, the need for the loop around the wait is that the wait can return spuriously (i.e. without being signaled). The claim is that it is far less expensive to implement a condition variable that can return spuriously from a wait, as opposed to one that doesn't. And most clients of condition variable will ignore spurious wake ups anyway. So those clients should not have to pay for a condition variable that is guaranteed to not return spuriously.Nomination
@Howard : It's the phrase "without performance penalty" that is confusing.Interjoin
F
7

If you don't want to write the loop you can use the overload that takes the predicate instead:

cv.wait(lock, [&q]{ return !q.is_empty(); });

It is defined to be equivalent to the loop, so it works just as the original code.

Ferrigno answered 15/6, 2012 at 0:31 Comment(2)
(For the record.) My objection to this solution was that it still has a polling loop, hidden inside the implementation of wait. If my code is going to loop, I'd actually prefer the explicit while loop.Lowson
@Lowson : "Equivalent to" is very far removed from "implemented in terms of" in the C++ standard. "Equivalent to" explains the expected semantics/outcome, but that doesn't imply that there's a literal polling loop here in any way.Interjoin
A
4

Even if you could do this, the C++11 spec allows cv.wait() to unblock spuriously (to account for platforms that have that behavior). So even if there are no vulture threads (setting aside the argument about whether or not they should exist), the consumer thread can't expect there to be a cookie waiting, and still has to check.

Afterbrain answered 15/6, 2012 at 0:30 Comment(0)
L
0

I think this is not safe:

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    cv.notify_one();
}

You are still holding the lock when you notify another thread waiting for the lock. So it could be that the other thread immediately awakes and tries to get the lock before the destructor called after cv.notify_one() releases the lock. That means the other thread goes back to wait eventually for ever.

So I think this should coded as:

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    lock.unlock();
    cv.notify_one();
}

or if you don't like to unlock manually as

void producer_thread() {
    {
        std::unique_lock<std::mutex> lock(m);
        q.push(42);
    }
    cv.notify_one();
} 
Lyte answered 12/5, 2015 at 12:25 Comment(2)
is that true? --Expellant
According to en.cppreference.com/w/cpp/thread/condition_variable it is not needed to hold the lock the notifcation. But they only say "not needed" - so this is at least not incorrect. I've seen helgrind (valgrind) reports about a missing lock if you notify though.Dyadic

© 2022 - 2024 — McMap. All rights reserved.