0

In case I'm using a cv without a predicate. I don't quite understand the use of the unique_lock that we pass in. I checked this link: Why does a condition variable need a lock (and therefore also a mutex) and more links from that.

Most of the explanations suggested that the mutex was used to get a lock on the predicate that was being passed into the CV. So, in case I don't use a predicate function, why is the lock required?

A hypothetical code without a lock could look something this:

condition_variable cv;
mutex dataMutex;
string sharedData;
atomic<bool> flag = false;

void doTask(){

    while(!flagSet){ // To prevent spurious wake-up
        cv.wait(); //Just wait, no lock/predicate passed in.
    }
    
    lock_guard<mutex> lock(dataMutex); //Get a lock and read shared data
    cout << sharedData;
}

void createTask(){
    lock_guard<mutex> lock(dataMutex); //Get a lock and update the shared data
    sharedData = "abc";
    flag = true;
    
    cv.notify_all();

}

int main(){
    thread a(doTask, 10);
    sleep(seconds(5));
    thread b = thread(createTask);
    
    // Join the threads and return.
}

Possibly the cv could internally use a mutex to synchronize the wait and notify, but why does it need an external one. Could anyone please help understand the use of an external lock when I don't have a predicate?

jay
  • 13
  • 7
  • 1
    It was really exhaustively explained in one of discussions from your link https://stackoverflow.com/a/2763749/4165552 . What more do you need? – pptaszni Jul 26 '23 at 09:09
  • "Possibly the cv could internally use a mutex to synchronize the wait and notify" - no, they are designed to be stateless. – pptaszni Jul 26 '23 at 09:15
  • 1
    Without the lock your code has a race condition leading to it waiting forever when `flag` is unset when it's checked but set by the time `wait` is called – Alan Birtles Jul 26 '23 at 09:16
  • Though with `atomic` you can avoid the condition: https://en.cppreference.com/w/cpp/atomic/atomic/wait – Alan Birtles Jul 26 '23 at 09:20
  • 1
    How do you synchronize between others when you don't input the lock? If you don't need synchronization, then CV isn't needed as well.. – Louis Go Jul 26 '23 at 09:26
  • @AlanBirtles, so, in case there were no spurious wakeups, I don't really need a lock? I did use atomic for the flag.. You meant anywhere else? – jay Jul 26 '23 at 09:39
  • _"Possibly the cv could internally use a mutex to synchronize the wait and notify."_ That wouldn't help. The role of the mutex is to synchronize wait (loop) and the change of the waiting condition (whichever it is). So the user needs this mutex to change the waiting condition when it is locked. – Daniel Langr Jul 26 '23 at 09:40
  • @jay No, the problem is that if the notification comes between checking `flagSet` and executing `cv.wait()`, then the waiting never ends. – Daniel Langr Jul 26 '23 at 09:41
  • @LouisGo, I did have the mutex to lock the sharedData, others can just block till the thread holding it lets it go. – jay Jul 26 '23 at 09:42
  • BTW, the problem is unrelated to whether you `wait` with a predicate or without. The overload with a predicate is just a shortcut for waiting without it in a `while` loop, which is exactly what you are doing. – Daniel Langr Jul 26 '23 at 09:49
  • @DanielLangr, hypothetical if spurious wakeups were absent and I don't need a predicate, the cv wouldn't need a lock? The lock can be placed while modifying the data, call notify, and the receiver locks again? – jay Jul 26 '23 at 09:55
  • 1
    No, this is nothing to do with spurious wake-ups, the race condition occurs with or without them – Alan Birtles Jul 26 '23 at 09:58
  • Possible scenario: 1) thread `a` reads `flagSet` as false. 2) thread `b` sets `flagSet` to true. 3) thread `b` calls `cv.notify_all()` but the notification "gets lost". 4) thread `a` calls `cv.wait()`. Now the waiting never ends (race condition/deadlock). – Daniel Langr Jul 26 '23 at 10:15
  • So, looks like the job of the predicate is not only for the spurious calls, but also to help the programmer avoid deadlocks. So, it's like, when you're about to call a notify, get the shared mutex. In case anyone else is about to start waiting on this, first try get the lock. In this case, they can't, since notify-er is holding it. Once the notify is called, the lock is released and wait-er can check the predicate and go ahead without actually waiting. In case notify is not about to be called, the wait-er will get the mutex immediately, check the predicate and run or wait on that. True? – jay Jul 26 '23 at 10:46
  • 1
    If you take out the red herring about spurious calls, then it's basically true. – Eljay Jul 26 '23 at 11:23
  • 1
    In this case `std::future` + `std::promise` would be the more appropriate class to use. `std::latch`, `std::barrier`, `std::counting_semaphore`, ect. may be other alternatives that could be more suitable than `std::condition_variable`, depending on how the threads should interact... – fabian Jul 26 '23 at 17:19
  • I actually was using promises till now. Had to change to CVs for a better design – jay Jul 27 '23 at 04:28

0 Answers0