4

While implementing a thread pool pattern in C++ based on this, I came across a few questions.

Let's assume minimal code sample:

std::mutex thread_mutex;
std::condition_variable thread_condition;
void thread_func() {
    std::unique_lock<std::mutex> lock(thread_mutex);
    thread_condition.wait(lock);
    lock.unlock();
}

std::thread t1 = std::thread(thread_func);
  1. Regarding cppreference.com about conditon_variable::wait(), wait() causes the current thread to block. What is locking the mutex then for when I only need one thread at all using wait() to get notified when something is to do?
  2. unique_lock will block the thread when the mutex already has been locked by another thread. But this wouldn't be neccesary as long as wait() blocks anyway or what do I miss here?

Adding a few lines at the bottom...

std::thread t2 = std::thread(thread_func);
thread_condition.notify_all()
  1. When unique_lock is blocking the thread, how will notify_all() reach both threads when one of them is locked by unique_lock and the other is blocked by wait()? I understand that blocking wait() will be freed by notify_all() which afterwards leads to unlocking the mutex and that this gives chance to the other thread for locking first the mutex and blocking thread by wait() afterwards. But how is this thread notified than?

Expanding this question by adding a loop in thread_func()...

std::mutex thread_mutex;
std::condition_variable thread_condition;
void thread_func() {
    while(true) {
        std::unique_lock<std::mutex> lock(thread_mutex);
        thread_condition.wait(lock);
        lock.unlock();
    }
}

std::thread t1 = std::thread(thread_func);
std::thread t2 = std::thread(thread_func);
thread_condition.notify_all()
  1. While reading documentation, I would now expect both threads running endlessly. But they do not return from wait() lock. Why do I have to use a predicate for expected behaviour like this:
bool wakeup = false;
//[...]
        thread_condition.wait(lock, [] { return wakeup; });
//[...]
wakeup = !wakeup;
thread_condition.notify_all();

Thanks in advance.

Rhino R.
  • 81
  • 7
  • 5
    Mutex for condition variable is to [protect the condition_variable](https://stackoverflow.com/questions/2763714/why-do-pthreads-condition-variable-functions-require-a-mutex) itself. Note that [wait](https://en.cppreference.com/w/cpp/thread/condition_variable/wait) unlocks the unique lock – pptaszni Dec 08 '22 at 18:52
  • 1
    And here nice explanation why we need the condition_variable even though we already have mutex that can also block https://stackoverflow.com/questions/12551341/when-is-a-condition-variable-needed-isnt-a-mutex-enough – pptaszni Dec 08 '22 at 18:54
  • 1
    4. `notify_all()` *will* release the threads (assuming they have reached the wait already), but they will then both just loop around and wait again. They aren't *doing* anything, except waiting. – BoP Dec 08 '22 at 19:02
  • 1
    @pptaszni Well, this is for what I like stackoverflow. This answers the first three questions and left only the fourth open at least assuming C++ threads work similar to posix threads (what I do for now). To summarize this for the future and feel free to correct me: `wait()` unlocks the mutex and allows other to access the mutex. So more than one thread can kind of listening to `wait()`. Locking a mutex before ensures that next `wait()` will not be called until the active `wait()` call finished. Thanks for your fast answers. – Rhino R. Dec 08 '22 at 19:06
  • @BoP Yes, in this example they do nothing than waiting. But adding e.g. `std::cout << "a";` after `wait()` doesn't output anything until predicate is added. – Rhino R. Dec 08 '22 at 19:07
  • 1
    Then my guess is that the main thread runs fast, and passes `notify_all` before the other thread have reached the wait. Only threads that are waiting at that point will be notified. If you do additional stuff before the call, the other threads get a head start. – BoP Dec 08 '22 at 19:13
  • @BoP Thanks for this wild guess. I added `std::this_thread::sleep_for(std::chrono::milliseconds(500));` before calling `notify_all()` and you're right, it was too fast. You both helped me a lot in understanding `condition_variable` better. – Rhino R. Dec 08 '22 at 19:18
  • Just for my further understanding ... I do not have to call `lock.unlock()` at all when mutex is unlocked by `wait()` already, haven't I? – Rhino R. Dec 08 '22 at 19:26
  • 2
    When a thread leaves the `wait()`, it will re-lock the mutex, so it is still/again locked at that point. On the other hand, the destructor of `lock` will also unlock the mutex, if it still holds a lock. – BoP Dec 08 '22 at 20:24
  • @RhinoR. "Locking a mutex before ensures that next wait() will not be called until the active wait() call finished" - yes, except I wouldn't say `call finished`, because wait returns when it is woken up. Locking the mutex ensures that 2 different threads can't call `thread_condition.wait` at exactly the same time possibly causing data race in `condition_variable` internal logic. I am not sure about your 4th point. Is it that `wait()` is called after `notify` and then never returns? – pptaszni Dec 09 '22 at 13:57
  • Thanks for clarifying this. I would appreciate if documentations like cppreference.com would be that clear about it. This and e.g. the re-lock of mutex by `wait()` is not stated and cannot implied by any part of the `wait()` documentation. The 4th point was about impatience. `wait()` was called and "afterwards" I called `notify`. But I didn't expected how slow a thread can start so `notify()` was internally called before the started thread has reached to `wait()`. – Rhino R. Dec 09 '22 at 15:46
  • @RhinoR.: CPPReference very much [does describe the unlock/lock done by `wait`](https://en.cppreference.com/w/cpp/thread/condition_variable/wait). – Davis Herring Dec 11 '22 at 04:58

1 Answers1

4

This is really close to being a duplicate, but it's actually that question that answers this one; we also have an answer that more or less answers this question, but the question is distinct. I think that an independent answer is needed, even though it's little more than a (long) definition.

What is a condition variable?

The operational definition is that it's a means for a thread to block until a message arrives from another thread. A mutex alone can't possibly do this: if all other threads are busy with unrelated work, a mutex can't block a thread at all. A semaphore can block a lone thread, but it's tightly bound to the notion of a count, which isn't always appropriate to the nature of the message to receive.

This "channel" can be implemented in several ways. Very low-tech is to use a pipe, but that involves expensive system calls. Windows provides the Event object which is fundamentally a boolean on whose truth a thread may wait. (C++20 provides a similar feature with atomic_flag::wait.)

Condition variables take a different approach: their structural definition is that they are stateless, but have a special connection to a corresponding mutex type. The latter is necessitated by the former: without state, it is impossible to store a message, so arrangements must be made to prevent sending a message during some interval between a thread recognizing the need to wait (by examining some other state: perhaps that the queue from which it wants to pop is empty) and it actually being blocked. Of course, after the thread is blocked it cannot take any action to allow the message to be sent, so the condition variable must do so.

This is implemented by having the thread take a mutex before checking the condition and having wait release that mutex only after the thread can receive the message. (In some implementations, the mutex is also used to protect the workings of the condition variable, but C++ does not do so.) When the message is received, the mutex is re-acquired (which may block the thread again for a time), as is necessary to consult the external state again. wait thus acts like an everted std::unique_lock: the mutex is unlocked during wait and locked again afterwards, with possibly arbitary changes having been made by other threads in the meantime.

Answers

Given this understanding, the individual answers here are trivial:

  1. Locking the mutex allows the waiting thread to safely decide to wait, given that there must be some other thread affecting the state in question.
  2. If the std::unique_lock blocks, some other thread is currently updating the state, which might actually obviate the need for wait.
  3. Any number of threads can be in wait, since each unlocks the mutex when it calls it.
  4. Waiting on a condition variable, er, unconditionally is always wrong: the state you're after might already apply, with no further messages coming.
Davis Herring
  • 36,443
  • 4
  • 48
  • 76