Sreepathy Jayanand
Sreepathy Jayanand

Reputation: 89

What happens when a conditional variable gets notified but doesn't have the lock yet?

std::condition_variable cv;
std::mutex m_cnt;
int cnt = 0;
void producer() {
    std::unique_lock<std::mutex> ul(m_cnt);
    cnt++;
    cv.notify_one();
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
void consumer() {
    std::unique_lock<std::mutex> ul(m_cnt);
    cv.wait(ul, [] {return cnt > 0;}); // ---> Here
    cnt--;
}

In the following example, what happens in cv.wait()? It has been notified by producer, but doesn't have the m_cnt(lock) to move further. Once the consumer is "notified" does it keep on trying to acquire the lock indefinitely?

Upvotes: 3

Views: 114

Answers (3)

Howard Hinnant
Howard Hinnant

Reputation: 219325

I'm going to answer the question in the title first:

Question:

What happens when a conditional variable gets notified but doesn't have the lock yet?

Answer: The notification is missed. If a (un-timed) wait is initiated after the only notification, it might wait forever. It also might not because spurious wakes are allowed (waking for no reason).

However: The code you posted does not have this problem.

Details:

Let's say (as you imply) that producer() runs first.

  1. It locks the mutex.
  2. It increments cnt.
  3. It notifies cv, with no one waiting on it, so that notification is missed.
  4. It sleeps for 1s.
  5. It releases the mutex.

And let's say that consumer() starts running any time after step 1 in producer():

  1. It blocks on the mutex until producer() releases it.
  2. It locks the mutex.
  3. It calls cv.wait(ul, [] {return cnt > 0;}).

At this point it is instructive to look at the spec for this function.

Effects: Equivalent to:

while (!pred())
    wait(lock);

Note that !pred() is called before a wait actually happens! The predicate will find that cnt has already been incremented by producer() and thus skip the wait.

So the missed notification is immaterial.

  1. Decrement cnt.
  2. Release the mutex.

If consumer() runs first, then it will enter the wait and producer() will have either not started yet, or it will be blocked on the mutex until consumer() enters the wait. Then producer() will increment cnt (step 2) and continue through step 5. consumer() will receive the notification and wait until producer()'s step 5 is done before proceeding with consumer()'s step 4.

Upvotes: 1

Solomon Slow
Solomon Slow

Reputation: 27190

Your consumer calls cv.wait(ul, []{return cnt>0;}). That function call unlocks m_cnt (via ul) while it awaits a notfication, but then it re-locks m_cnt before it loops back to re-test cnt>0.

Since the producer has m_cnt locked when it calls cv.notify_one(), here's what happens:

    producer thread                 consumer_thread
    -----------------------------   -------------------------------------
                                    locks m_cnt
    blocks, trying to lock m_cnt.
                                    enters cv.wait(...)
                                     |  tests cnt>0, finds it to be false
                                     |  releases m_cnt
    locks m_cnt                      |  sleeps, awaiting notification
    cnt++                            |
    cv.notify_one()                  |
    sleep_for(1 second)              |  receives notification
                                     |  blocks, trying to acquire `m_cnt`
          ...                        |      ...
    awakes from sleep,               |
    unlocks m_cnt                    |
    returns                          |  acquires m_cnt
                                     |  tests cnt>0, finds it to be true
                                     |  releases m_cnt
                                    returns from cv.wait(...)
                                    cnt--;
                                    returns

Update: You asked,

Does the consumer keep on trying to obtain m_cnt forever, or does it sleep and has to be notified again?

cv.wait(ul, [] {return cnt > 0;}) will not return until two conditions are both met:

  1. ul is locked by the consumer, and
  2. cnt > 0.

The two argument form of the wait call loops until both conditions are met. Effectively, it works like this (pseudocode):

while (! (cnt > 0)) {
    SIMULTANEOUSLY call ul.unlock() AND start waiting to be notified.
    wake up/notified
    call ul.lock()
}

"SIMULTANEOUSLY" is my way of saying that it is guaranteed not to miss any notification that could happen in between the moment when the consumer releases the mutex, and the moment when the consumer is able to be notified.

"waiting to be notified" means sleeping until a notification happens.

What happens if this wait was 1000 seconds?

It will happen exactly as in the side-by-side description that I showed you previously, except change "sleep_for(1 second)" to "sleep_for(1000 seconds)." Either way, the cv.wait(ul, [] {return cnt > 0;}) call in the consumer cannot return until the producer function releases the mutex, which, it will not do until after the sleep_for call returns.

Upvotes: 2

CygnusX1
CygnusX1

Reputation: 21818

https://en.cppreference.com/w/cpp/thread/condition_variable/wait

void wait( std::unique_lock<std::mutex>& lock );

...

wait(), when unblocked (called notify_one() or notify_all() by other thread), calls lock.lock() again (possibly blocking on the lock), then returns.

The same mechanism happens with wait(lock, predicate). When notified it tries to relock the lock and continues with the predicate, and possibly with the rest of the program when that lock is actually gained.

So, yes, it is working as you describe - the condition variable is called and itself is no longer blocking, but locking the mutex will block until the mutex becomes available.

Upvotes: 1

Related Questions