What's the correct way to deal with spurious wakeups, in general?
Asked Answered
B

2

5

Among the options below, is there any correct way to deal with spurious wakeups when using conditional variables?

1) Put the wait(unique_lock_ul) into an infinite while loop, using a boolean

unique_lock<mutex> ul(m); 
while(!full)
  cv.wait(ul);

2) Same with if

unique_lock<mutex> ul(m); 
if(!full)
  cv.wait(ul);

3) Put a condition inside the wait(), for example by using lambda functions

unique_lock<mutex> ul(m); 
cv.wait(ul, [&](){return !full;});

If none of this is correct, how does one deal with spurious wakeups easily?

I'm rather new to conditional variables in C++ and I'm not sure if some of the code I read deals with the case of spurious wakeups or not.

Bashee answered 5/9, 2018 at 19:29 Comment(2)
1) and 2) are the same code. Did you mean to show something different?Lias
Yes, sorry, I just modifiedBashee
P
6

The short answer is, your code may be correct or wrong; you didn't show exactly how full is manipulated.

Individual bits of C++ code are never thread safe. Thread safety is a relational property of code; two bits of code can be thread safe with respect to each other if they can never cause a race condition.

But one bit of code is never thread safe; saying something is thread safe is like saying something is "the same height".


The "monkey see monkey do" condition variable pattern is this:

template<class T>
class cv_bundle {
  std::mutex m;
  T payload;
  std::condition_variable cv;
public:
  explicit cv_bundle( T in ):payload(std::move(in)) {}

  template<class Test, class Extract>
  auto wait( Test&& test, Extract&& extract ) {
    std::unique_lock<std::mutex> l(m);
    cv.wait( l, [&]{ return test(payload); } );
    return extract(payload);
  }
  template<class Setter>
  void load( Setter&& setter, bool only_one = true ) {
    std::unique_lock<std::mutex> l(m);
    bool is_set = setter( payload );

    if (!is_set) return; // nothing to notify
    if (only_one)
      cv.notify_one();
    else
      cv.notify_all();
  }
};

test takes a T& payload and returns true if there is something to consume (ie, the wakeup is not-spurious).

extract takes a T& payload and returns whatever information you want from it. It should reset the payload usually.

setter modifies the T& payload in a way that test will return true. If it does so, it returns true. If it chooses not to, it returns false.

All 3 get called within a mutex locking access to the T payload.

Now, you can generate variations on this, but doing so is very hard to get right. Do not, for example, assume an atomic payload means you don't have to lock a mutex.

While I bundled these 3 things together, you could use a single mutex for a pile of condition variables, or use the mutex for more than just the condition variable. The payload could be a bool, a counter, a vector of data, or something more alien; in general, it must always be protected by the mutex. If it is atomic, during some point in the open interval between the value being modified and the notification the mutex must be locked, or you risk losing the notification.

Manual loop control instead of passing in the lambda is a modification, but describing what kind of manual loops are legal and which are race conditions is a complex problem.

In effect, I avoid leaving this monkey-see monkey-do "cargo cult" style of use of condition variables unless I have extremely good reason. And then I am forced to read up on the C++ memory and threading model, which doesn't make my day, and it means my code is most likely not going to be correct.

Note that if any of the lambdas passed in go and call back into cv_bundle the code I showed is no longer valid.

Phyllode answered 5/9, 2018 at 19:48 Comment(1)
@ericfrazer yes, threading is hard. And almost every language that pretends it is easy is lying to you.Phyllode
S
3

Either 1 or 3 way is fine for dealing with spurious wakeups (assuming that full modification is protected by the same mutex) except you get predicate condition wrong, it should be:

unique_lock<mutex> ul(m); 
cv.wait(ul, [&](){return full;});

to make this code equal to variant 1.

Variant 2 is not fine though as on spurious wakeup wait condition would not be rechecked, unlike 2 other cases.

Sclerite answered 5/9, 2018 at 19:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.