1

I have two threads. One thread acts as a timer thread which at regular intervals of time needs to send a notification to another thread. I intend to use C++ condition variables. (There is a good article on how to use C++ condition variables along with its traps and pitfalls in the following link)

I have the following constraints/conditions :-

  1. The notifying thread need not lock on to a mutex
  2. The notified (or the receiver) thread does some useful section but there is no critical section
  3. The receiver thread is allowed to miss a notification if and only if it is doing useful work
  4. There should be no spurious wakeups.

Using the above link as a guideline I put together the following piece of code

// conditionVariableAtomic.cpp

#include <atomic>
#include <condition_variable>
#include <iostream>
#include <thread>
#include <iostream>       // std::cout, std::endl
#include <thread>         // std::this_thread::sleep_for
#include <chrono>         // std::chrono::seconds


std::mutex mutex_;
std::condition_variable condVar;

std::atomic<bool> dataReady{false};

void waitingForWork(){
    int i = 0;
    while (i++ < 10)
    {
        std::cout << "Waiting " << std::endl;
        {
            std::unique_lock<std::mutex> lck(mutex_);
            condVar.wait(lck, []{ return dataReady.load(); });   // (1)
            dataReady = false;
        }
        std::cout << "Running " << std::endl;
        // Do useful work but no critical section.
    }
}

void setDataReady(){
    int i = 0;
    while (i++ < 10)
    {
        std::this_thread::sleep_for (std::chrono::seconds(1));
        dataReady = true;
        std::cout << "Data prepared" << std::endl;
        condVar.notify_one();
    }
}

int main(){
    
  std::cout << std::endl;

  std::thread t1(waitingForWork);
  std::thread t2(setDataReady);

  t1.join();
  t2.join();
  
  std::cout << std::endl;
  
}

I use an atomic predicate to avoid spurious wakeups, but don't use a lock_guard in the notifying thread. My question is:

  1. does the above piece of code satisfy the constraints/conditions listed above?
  2. I understand that the receiver thread cannot avoid a mutex, hence the need to use std::unique_lock<std::mutex> lck(mutex_); in the receiver. I have however limited the scope of std::unique_lock<std::mutex> lck(mutex_); i.e. put the following section of code
    std::unique_lock<std::mutex> lck(mutex_);
    condVar.wait(lck, []{ return dataReady.load(); });   // (1)
    dataReady = false;
    
    inside a scope block aka { .... } so that the mutex is unlocked as soon as the wait condition is over (the receiver then does some useful work but since there is no critical section, it does not need to hold on to the mutex for the entire while loop). Could there still be consequences/side effects of this limited scoping in this context ? Or does the unique_lock<std::mutex> need to be locked for the entire while loop?
Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
bobbydev
  • 525
  • 3
  • 12

1 Answers1

3

Your code has a race condition. Between checking the value of dataReady in your wait predicate and actually starting the wait, the other thread can set dataReady and call notify_one. In your example this isn't critical as you'll just miss one notify and wake up a second later on the next one.

Another race condition is that you can set dataReady to true in one thread, set dataReady back to false in the other thread and then call notify_one in the first thread, again this will cause the wait to block for longer than you intended.

You should hold the mutex in both threads when setting dataReady and using the condition variable to avoid these races.

You could avoid the second race condition by using an atomic counter instead of a boolean, incrementing it on one thread then decrementing on the other and in the predicate checking if it is non-zero.

Alan Birtles
  • 32,622
  • 4
  • 31
  • 60
  • Say I am interested in avoiding only the second race condition and I use an atomic counter as you have suggested. Would I still need to hold the mutex in the notifying thread ? If not, then how do we avoid this race condition - one thread increments this counter from 0 to 1, other thread decrements it back to 0 and then first thread calls notify_one. Would this not cause wait to block longer (just if we were using a boolean) ? – bobbydev Sep 21 '20 at 06:19
  • depends how you implement it, if you decrement the counter the same number of times you increment it (i.e. increment when one item of data is ready, decrement when you've processed that item) then the waiting thread should never be able to decrement to `0` whilst there is still data ready. – Alan Birtles Sep 21 '20 at 06:23
  • Thanks Alan, I see your point. I am also thinking this through - this second race condition you mentioned would happen when the receiver is falling behind and there are notifications getting queued up. If the receiver task finishes its job in less than a second this race condition is not likely. Please correct me if I am wrong. – bobbydev Sep 21 '20 at 07:45
  • Neither race condition is likely but even an unlikely event will happen eventually if you run your program for long enough – Alan Birtles Sep 21 '20 at 08:03
  • @PiK With `condition_variable` you always have to use a mutex to avoid missing a notification (note though, that it is usually not necessary to hold the mutex during the notify call itself). If you want to avoid locks altogether, you have to wait for atomic_wait/atomic_notify introduced in C++20: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p1135r5.html – mpoeter Sep 21 '20 at 12:21
  • What about promise and future ? Could it be used in lieu of an atomic wait ? future does have an associated wait()/wait_for() as well – bobbydev Oct 04 '20 at 22:21
  • Yes, though only as a one off,i can't see a good way to implement this code with promises – Alan Birtles Oct 05 '20 at 06:21
  • @AlanBirtles, I agree about the one off limitation with promises (as a promise can be fulfilled only once). But what are your thoughts about the approach in this post (I posted it as a separate post and seems to work) - https://stackoverflow.com/questions/64258821/use-of-promise-and-future-to-provide-continous-periodic-notifications-from-one-t – bobbydev Oct 08 '20 at 08:42