Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding condition_variable::wait for blocking a thread

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.

like image 969
Rhino R. Avatar asked Jul 30 '26 21:07

Rhino R.


1 Answers

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.
like image 190
Davis Herring Avatar answered Aug 02 '26 11:08

Davis Herring