SungJinKang
SungJinKang

Reputation: 429

Notifying condtion_variable without unlocking mutex still works well. Why?

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
 
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
 
void worker_thread()
{
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});
 
    std::cout << "Worker thread is processing data\n";
    data += " after processing";
 
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";
 
    //lk.unlock(); /// here!!!!!!!!!!!
    cv.notify_one();
}
 
int main()
{
    std::thread worker(worker_thread);
 
    data = "Example data";
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();
 
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';
 
    worker.join();
}

I don't know why this code works without unlock before notify_one in worker_thread.
I think if i don't unlock before notify, Waked up main thread will block again because mutex is still held by worker_thread.
After that worker_thread will unlock mutex(because unique_lock unlock mutex when destroyed).
Then No one can wake up sleeping main thread.

But this code works well without unlocking mutex before notify.
How this works???? (I read cppreference comments, but i couldn't understand it)

Upvotes: 0

Views: 756

Answers (1)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275800

There are two things to talk about here. First, why it works, and second, why you don't want to call unlock first.

It works because cv.wait(lk, []{return processed;}); actually unlocks lk while waiting for a notification.

Some sequences. Main gets lock first:

 MAIN                       WORKER
 auto lk = lock();          
                            auto lk = lock(); (blocks)
 cv.wait(lk, condition);
 checks condition; fails
 releases lk
                            wakes up with lk
                            fullfill condition
                            cv.notify_one();
 wakes up from notify
 tries to reget lk, blocks
                            lk.unlock();
 wakes up with lk.
 checks condition; passes

Worker gets lock first:

 MAIN                       WORKER
                            auto lk = lock();
 auto lk = lock(); (blocks)         
                            fullfill condition
                            cv.notify_one();
                            lk.unlock();
 wakes up with lk.
 cv.wait(lk, condition);
 checks condition; passes

for the case where we unlock first:

 MAIN                       WORKER
 auto lk = lock();          
                            auto lk = lock(); (blocks)
 cv.wait(lk, condition);
 checks condition; fails
 releases lk
                            wakes up with lk
                            fullfill condition
                            lk.unlock();
                            cv.notify_one();
 wakes up from notify
 gets lk
 checks condition; passes

Worker gets lock first, two possibilities at the end:

 MAIN                       WORKER
                            auto lk = lock();
 auto lk = lock(); (blocks)         
                            fullfill condition
                            lk.unlock();
 wakes up with lk.
 cv.wait(lk, condition);
 checks condition; passes
                            cv.notify_one(); (nobody cares)

 MAIN                       WORKER
                            auto lk = lock();
 auto lk = lock(); (blocks)         
                            fullfill condition
                            lk.unlock();
                            cv.notify_one(); (nobody cares)
 wakes up with lk.
 cv.wait(lk, condition);
 checks condition; passes

Now, why is it better to hold the lock?

Because the writers of C++ standard libraries made it better. The library knows that the cv waiter is associated with a mutex, and knows this thread currently holds it.

So what actually happens is:

 MAIN                       WORKER
 auto lk = lock();          
                            auto lk = lock(); (blocks)
 cv.wait(lk, condition);
 checks condition; fails
 releases lk
                            wakes up with lk
                            fullfill condition
                            cv.notify_one(); // knows listener holds mutex, waits for
                            lk.unlock(); // and actually wakes up listening threads
 wakes up from notify
 gets lk
 checks condition; passes

Now there are far more possibilities than the above. condition_variable has "spurious" wakeups, where you are woken up even though nobody notified you. So discipline has to be followed in using it.

The general rule is that both sides must share a mutex, and the lock must be held at any point between the test condition changed and the notification. And optimally, the lock should be held until immediately after the notification is sent.

This in addition to preventing race conditions on the condition state itself. But if you use an atomic condition state, it avoids race conditions on the state, but it doesn't satisfy the locking requirements of condition variable.

The above rule -- hold the lock sometime in that interval (possibly for the entire interval) -- is a simplification, but is sufficient to guarantee you don't lose notifications. The full rule means going back to the memory model of C++ and doing painful proofs about what your code does, and I honestly don't want to do that again. So I use that rule of thumb.

To illustrate what can go wrong with an atomic condition "state" and no lock;

 MAIN                       WORKER
 auto lk = lock();
 cv.wait(lk, condition);
 checks condition; fails
                            fullfill condition
                            cv.notify_one(); (nobody cares)
 goes to sleep, releases lk

and nothing ever happens again.

Upvotes: 1

Related Questions