How do std::unique_lock and std::condition_variable work
Asked Answered
O

3

9

I need to be clarified how lock and condition_variable work.

In the -slightly modified- code from here cplusplusreference

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;

void worker_thread()
{
    // Wait until main() sends data
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});

    // after the wait, we own the lock.
    std::cout << "Worker thread is processing data\n";
    data += " after processing";

    // Send data back to main()
    processed = true;
    std::cout << "Worker thread signals data processing completed\n";

    // Manual unlocking is done before notifying, to avoid waking up
    // the waiting thread only to block again (see notify_one for details)
    lk.unlock();
    cv.notify_one();
}

int main()
{
    std::thread worker(worker_thread);
    std::this_thread::sleep_for(std::chrono::seconds(1));

    data = "Example data";
    // send data to the worker thread
    {
        std::lock_guard<std::mutex> lk(m);
        ready = true;
        std::cout << "main() signals data ready for processing\n";
    }
    cv.notify_one();

    // wait for the worker
    {
        std::unique_lock<std::mutex> lk(m);
        cv.wait(lk, []{return processed;});
    }
    std::cout << "Back in main(), data = " << data << '\n';

    worker.join();
}

The thing that I was confused with was how main thread could lock the mutex if worker_thread had already locked it.

From this answer I saw that it is because cv.wait unlocks the mutex.

But now I am confused with this: So why do we need to lock it at all, if cv.wait will unlock it?

For example, could I do this?

std::unique_lock<std::mutex> lk(m, std::defer_lock);

So, I create the lock object because cv needs it, but I do not lock it when I create.

Is there any difference now?

I did not see why I get "runtime error" here in that case.

Obmutescence answered 10/8, 2015 at 7:11 Comment(0)
G
4

I will try to add a bit more explanation as to WHY condition variables require a lock.

You have to have a lock because your code needs to check that the condition predicate is true. The predicate is some value or combination of values that has to be true in order to continue. It could be a pointer that is NULL or points to a completed data structure ready for use.

You have to lock it AND CHECK the predicate before waiting because by the time you start waiting for the condition another thread might have already set it.

The condition notify and the wait returning does NOT mean that the condition is true. It only means that the condition WAS true at some time. It might even have been true, then false, then true again. It might also mean that your thread had been in an unrelated signal handler that caused the condition wait to break out. Your code does not even know how many times the condition notify has been called.

So once the condition wait returns it LOCKS the mutex. Now your code can check the condition while safely in the lock. If true then the code can update what it needs to update and release the lock. If it wasn't true it just goes back to the condition wait to try again. For example, it could take that data structure pointer and copy it into a vector, then set the lock protected pointer back to NULL.

Think of a condition as a way to make a polling loop more efficient. Your code still has to do all of the things it would do running in a loop waiting, except that it can go to sleep instead of non-stop spinning.

Gardiner answered 13/11, 2015 at 20:26 Comment(0)
G
9

Quoted from std::condition_variable::wait() :

Calling this function if lock.mutex() is not locked by the current thread is undefined behavior.

Gravimetric answered 10/8, 2015 at 10:4 Comment(0)
G
4

I will try to add a bit more explanation as to WHY condition variables require a lock.

You have to have a lock because your code needs to check that the condition predicate is true. The predicate is some value or combination of values that has to be true in order to continue. It could be a pointer that is NULL or points to a completed data structure ready for use.

You have to lock it AND CHECK the predicate before waiting because by the time you start waiting for the condition another thread might have already set it.

The condition notify and the wait returning does NOT mean that the condition is true. It only means that the condition WAS true at some time. It might even have been true, then false, then true again. It might also mean that your thread had been in an unrelated signal handler that caused the condition wait to break out. Your code does not even know how many times the condition notify has been called.

So once the condition wait returns it LOCKS the mutex. Now your code can check the condition while safely in the lock. If true then the code can update what it needs to update and release the lock. If it wasn't true it just goes back to the condition wait to try again. For example, it could take that data structure pointer and copy it into a vector, then set the lock protected pointer back to NULL.

Think of a condition as a way to make a polling loop more efficient. Your code still has to do all of the things it would do running in a loop waiting, except that it can go to sleep instead of non-stop spinning.

Gardiner answered 13/11, 2015 at 20:26 Comment(0)
A
1

I think your misunderstanding stems from a deeper misunderstanding of what locks are and how they interact with condition variables.

The basic reason a lock exists is to provide mutual exclusion. Mutual exclusion guarantees that certain parts of the code are only executed by a single thread. This is why you can't just wait with the lock until later - you need it locked to have your guarantees.

This causes problems when you want some other parts of the code to execute but still need to have mutual exclusion while the current piece of code executes. This is where condition variables come in handy: they provide a structured way to release the lock and be guaranteed that when you wake up again you will have it back. This is why the lock is unlocked while in the wait function.

Atheistic answered 10/8, 2015 at 10:5 Comment(11)
"be guaranteed that when you wake up again you will have it back" After the cv.notify, is it locked again automatically by the one that was waiting for cv? Did I understand correctly? What if meanwhile someone have locked it? Do it have to wait also again that, although there I only want it to wait for cv, but not to lock mutex again?Obmutescence
"When unblocked, regardless of the reason, lock is reacquired and wait exits.", quoted from en.cppreference.com/w/cpp/thread/condition_variable/waitAtheistic
Internally, I expect the condition variable's code makes an attempt to acquire the lock and blocks until the acquisition is made. Which would mean that yes, if some other thread managed to get the lock before the waiting thread, it will have to wait longer.Atheistic
Please provide an example for when you want to wait for the condition variable and not own the lock.Atheistic
I want to use the cond var like a deferred object. For example when I want to get the result of a function that is run in another thread which is already started before. Once I get the result, I am not interested to get lock again. Sorry now I write in hurry but if literally it is not clear I could provide code sample later.Obmutescence
I think I get what you mean, and I think you misunderstand the meaning of defer in this context. Usually when you construct a new unique_lock object it will attempt to acquire the relevant lock, according to the RAII idiom: en.wikipedia.org/wiki/Resource_Acquisition_Is_Initialization . When for some reason you don't want to acquire the the lock, you would pass std::defer_lock to the constructor to avoid that, and manually acquire the lock as necessary.Atheistic
:) No no, I didn't refer to std::defer_lock. I am trying to explain that I would like to use condition variable just like a deferred object.Obmutescence
Okay, I think I get it now. Do you want to use it like a Windows event, simply passing the information that an event has occurred to another thread that's waiting for it, but doesn't require the lock to handle the event?Atheistic
Yes, exactly like that!Obmutescence
I think you would be better off using other synchronization primitives for that if locking is a problem for you - this is a fundamental part of CV's design. Or just release the lock immediately after waking up from the wait.Atheistic
There can be a lot of race condition problems with Windows-style Event structures which the locking requirement of condition variables prevents. You have to do some fancy lock-free coding with correct operation ordering to use Event correctly with multiple waiters for the same Event. It's OK if you only ever have one thing looking at Event though.Gardiner

© 2022 - 2024 — McMap. All rights reserved.