Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does C++20 std::condition_variable not support std::stop_token?

In C++20 Standard library std::condition_variable_any::wait() family support std::stop_token for generalized thread cancellation, but std::condition_variable does not.

P0660R10 Stop Token and Joining Thread, Rev 10 says:

New in R6

  • User condition_variable_any instead of consition_variable to avoid all possible races, deadlocks, and unintended undefined behavior.

I think that the following code safely emulates condition_variable::wait() with stop_token cancellation. What am I missing? Is there subtle edge case?

template<class Lock, class Predicate>
bool cv_wait_with_stoken(
  std::condition_variable& cv, Lock& lock, std::stop_token stoken, Predicate pred)
{
  std::stop_callback callback{ stoken, [&cv]{ cv.notify_all(); } };
  while (!stoken.stop_requested()) {
    if (pred())
      return true;
    cv.wait(lock);
  }
  return pred();
}
like image 252
yohjp Avatar asked Oct 14 '22 22:10

yohjp


1 Answers

Yes, there is a race condition.

In general with a condition variable, you must hold the mutex for some period between modifying the guarded state (usually a variable) and signaling the condition variable. If you do not, you can miss a signal.

Making your state an atomic variable does not avoid that problem.

The wait code for a cv first checks the state. If it fails, it then atomically drops the lock and waits on a signal.

If your stop token is set in that gap after it is checked, but before it waits, then the stop token calls notify all, that notify all won't be picked up by the condition variable.

The cv.notify_all() would have to be preceded by getting that lock. And that opens up a whole can of worms.

Do not use this code, it will probably break horribly due to double locking or a a myriad of other things, but in theory it looks like:

bool cv_wait_with_stoken(
  std::condition_variable& cv,
  Lock& lock,
  std::stop_token stoken,
  Predicate pred
) {
  std::stop_callback callback{ stoken, [&cv]{
    lock.lock();
    lock.unlock();
    cv.notify_all();
  } };
  while (!stoken.stop_requested()) {
    if (pred())
      return true;
    cv.wait(lock);
  }
  return pred();
}

it may be possible to try_lock there, I would have to do a lot of hard proof work.

like image 172
Yakk - Adam Nevraumont Avatar answered Oct 18 '22 10:10

Yakk - Adam Nevraumont