Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do std::unique_lock and std::condition_variable work

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.

like image 840
Mert Mertce Avatar asked Aug 10 '15 07:08

Mert Mertce


3 Answers

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

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

like image 99
Richard Dally Avatar answered Oct 12 '22 09:10

Richard Dally


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.

like image 29
Zan Lynx Avatar answered Oct 12 '22 09:10

Zan Lynx


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.

like image 36
nitzanms Avatar answered Oct 12 '22 09:10

nitzanms