Reputation: 429
#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
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