6

On my neverending quest to understand std::contion_variables I've run into the following. On this page it says the following:

void print_id (int id) {
  std::unique_lock<std::mutex> lck(mtx);
  while (!ready) cv.wait(lck);
  // ...
  std::cout << "thread " << id << '\n';
}

And after that it says this:

void go() {
  std::unique_lock<std::mutex> lck(mtx);
  ready = true;
  cv.notify_all();
}

Now as I understand it, both of these functions will halt on the std::unqique_lock line. Until a unique lock is acquired. That is, no other thread has a lock.

So say the print_id function is executed first. The unique lock will be aquired and the function will halt on the wait line.

If the go function is then executed (on a separate thread), the code there will halt on the unique lock line. Since the mutex is locked by the print_id function already.

Obviously this wouldn't work if the code was like that. But I really don't see what I'm not getting here. So please enlighten me.

laurisvr
  • 2,724
  • 6
  • 25
  • 44
  • I don't think print_id and go should be using the same lock. The locks are there so that only one thread can be running print_id, and only one thread run go(). – Matthias Vegh May 13 '15 at 08:45
  • 1
    Hmmm I checked http://en.cppreference.com/w/cpp/thread/condition_variable and the producer and consumer both use the same lock. Odd. – Matthias Vegh May 13 '15 at 08:47
  • @Angew What do you mean by "protects the condition variable itself"? Is the condition variable not inherently thread safe? – laurisvr May 13 '15 at 08:51
  • 2
    @laurisvr Yeah, that was kinda poorly worded. Retracted. It's more like "it protects the `cv + ready` pair." – Angew is no longer proud of SO May 13 '15 at 08:54
  • 1
    @Matthias Vegh cppreference.com makes it a point to always unlock before notifying (mostly because I trust Concurrency in Action) – Cubbi May 13 '15 at 13:24

3 Answers3

10

What you're missing is that wait unlocks the mutex and then waits for the signal on cv.

It locks the mutex again before returning.

You could have found this out by clicking on wait on the page where you found the example:

At the moment of blocking the thread, the function automatically calls lck.unlock(), allowing other locked threads to continue.

Once notified (explicitly, by some other thread), the function unblocks and calls lck.lock(), leaving lck in the same state as when the function was called.

Community
  • 1
  • 1
molbdnilo
  • 64,751
  • 3
  • 43
  • 82
  • Ah thank you that clears things up:). My misunderstanding was because I thought the function calling the notify might not be able to get passed the mutex locking. But of course it can if the mutex has been unlocked by the wait already. – laurisvr May 14 '15 at 15:28
4

There's one point you've missed—calling wait() unlocks the mutex. The thread atomically (releases the mutex + goes to sleep). Then, when woken by the signal, it tries to re-acquire the mutex (possibly blocking); once it acquires it, it can proceed.

Notice that it's not necessary to have the mutex locked for calling notify_*, only for wait*

Angew is no longer proud of SO
  • 167,307
  • 17
  • 350
  • 455
  • 2
    Infact its probably a _pessimization_ to hold the lock while signalling a condition variable. The scheduler could potentially wake the waiting thread up when you notify it, then that thread fails to take the lock as the notifying thread still has it held potentially causing the thread to immediately yeild! This causes unnecessary thrashing. Obviously the scheduler _could_ be in cahoots with the CV code and know not to wake up the waiting thread unless it can also immediately get the mutex lock. NOTE: This is the opposite from pthreads_ CV which needs the mutex locked on notify – Mike Vine May 13 '15 at 09:41
  • @MikeVine The Linux implementation, at least, does not require that the mutex is held *during* the notification. However, you must lock the mutex *before* sending the notification, and unlock it *not before* making the change to the waiting condition (in this case, `ready`). I.e. for `mutex m`, `condition_variable v` and `atomic ready`, all of `m.lock(); ready=true; c.notify_one(); m.unlock();`, `m.lock(); ready=true; m.unlock(); c.notify_one();` and `ready=true; m.lock(); m.unlock(); c.notify_one();` are sufficiently synchronized. In this case, the third is probably most efficient. – Arne Vogel Aug 13 '18 at 14:56
1

To answer the question as posed, which seems necessary regarding claims that you should not acquire a lock on notification for performance reasons (isn't correctness more important than performance?): The necessity to lock on "wait" and the recommendation to always lock around "notify" is to protect the user from himself and his program from data and logical races. Without the lock in "go", the program you posted would immediately have a data race on "ready". However, even if ready were itself synchronized (e.g. atomic) you would have a logical race with a missed notification, because without the lock in "go" it is possible for the notify to occur just after the check for "ready" and just before the actual wait, and the waiting thread may then remain blocked indefinitely. The synchronization on the atomic variable itself is not enough to prevent this. This is why helgrind will warn when a notification is done without holding the lock. There are some fringe cases where the mutex lock is really not required around the notify. In all of these cases, there needs to be a bidirectional synchronization beforehand so that the producing thread can know for sure that the other thread is already waiting. IMO these cases are for experts only. Actually, I have seen an expert, giving a talk about multi-threading, getting this wrong — he thought an atomic counter would suffice. That said, the lock around the wait is always necessary for correctness (or, at least, an operation that is atomic with the wait), and this is why the standard library enforces it and atomically unlocks the mutex on entering the wait.

POSIX condition variables are, unlike Windows events, not "idiot-proof" because they are stateless (apart from being aware of waiting threads). The recommendation to use a lock on the notify is there to protect you from the worst and most common screwups. You can build a Windows-like stateful event using a mutex + condition var + bool variable if you like, of course.

Arne Vogel
  • 6,346
  • 2
  • 18
  • 31
  • 1
    Lock is not required for notifying but it is required for the change to the condition (indeed using atomic would be an error). – Cubbi May 13 '15 at 14:28