Reputation: 3135
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 ofconsition_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();
}
Upvotes: 9
Views: 2298
Reputation: 618
Instead of passing a std::unique_lock
, pass a std::mutex
.
The std::condition_variable
only accepts std::unique_lock<std::mutex>&
, so there is no need to use template parameters for it.
/**
* @pre The #mutex is not locked.
*/
template<class Predicate>
bool cv_wait_with_stoken(
std::condition_variable& cv, std::mutex& mutex, std::stop_token stoken, Predicate pred)
{
std::stop_callback callback{stoken, [&cv, &mutex] {
{ /// This locking is necessary to make sure the subsequent
/// notify_all() is not lost in region X.
std::lock_guard l(mutex);
}
cv.notify_all();
}};
std::unique_lock lock(mutex);
while (!stoken.stop_requested()) {
// Region X begins
if (pred())
return true;
// Region X ends
cv.wait(lock);
}
return pred();
}
Upvotes: 0
Reputation: 275740
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.
Upvotes: 4