This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of New status.
condition_variable{_any}::wait_{for, until}
should take timeout by valueSection: 32.7.4 [thread.condition.condvar], 32.7.5 [thread.condition.condvarany] Status: New Submitter: Hui Xie Opened: 2025-07-19 Last modified: 2025-07-26
Priority: Not Prioritized
View other active issues in [thread.condition.condvar].
View all other issues in [thread.condition.condvar].
View all issues with New status.
Discussion:
At the moment, both condition_variable
and condition_variable_any
's
wait_for
and wait_until
take the timeout time_point
/duration
by
const
reference. This can cause surprising behaviour. Given the following
example (thanks to Tim Song):
struct Task { system_clock::time_point deadline; // stuff }; std::mutex mtx; std::condition_variable cv; std::priority_queue<Task, vector<Task>, CompareDeadlines> queue; // thread 1 std::unique_lock lck(mtx); if (queue.empty()) { cv.wait(lck); } else { cv.wait_until(lck, queue.top().deadline); } // thread 2 std::lock_guard lck(mtx); queue.push(/* some task */); cv.notify_one();
From the user's point of view, it is sufficiently locked on both threads. However,
due to the fact that the time_point
is taken by reference, and that both libc++
and libstdc++'s implementation will read the value again after waking up, this
will read a dangling reference of the time_point
.
condition_variable{_any}::wait_{for, until}
.
Basically the user claims that these functions take time_point
/duration
by const
reference, if the user modifies the time_point
/duration
on another thread with
the same mutex, they can get unexpected return value for condition_variable
, and
data race for conditional_variable_any
.
Bug report here.
Reproducer (libstdc++ has the same behaviour as ours) on godbolt.
std::mutex mutex; std::condition_variable cv; auto timeout = std::chrono::steady_clock::time_point::max(); // Thread 1: std::unique_lock lock(mutex); const auto status = cv.wait_until(lock, timeout); // Thread 2: std::unique_lock lock(mutex); cv.notify_one(); timeout = std::chrono::steady_clock::time_point::min();
So basically the problem was that when we return whether there is no_timeout
or timeout
at the end of the function, we read the const
reference again, which can be changed since
the beginning of the function. For condition_variable
, it is "unexpected results" according
to the user. And in conditional_variable_any
, we actually unlock the user lock and acquire
our internal lock, then read the input again, so this is actually a data race.
wait_for
, the spec has
Effects: Equivalent to:
return wait_until(lock, chrono::steady_clock::now() + rel_time);
So the user can claim our implementation is not conforming because the spec says there needs
to be a temporary time_point
(now + duration
) created and since it should operate on this
temporary time_point
. There shouldn't be any unexpected behaviour or data race .
wait_until
it is unclear whether the spec has implications that implementations are allowed
to read abs_time
while the user's lock is unlocked.
it is also unclear if an implementation is allowed to return timeout
if cv
indeed does
not wait longer than the original value of timeout
. If it is not allowed, implementations
will have to make a local copy of the input rel_time
or abs_time
, which defeats the purpose
of taking arguments by const
reference.
For both of the examples, Ville has a great comment in the reflector:
It seems like a whole bag of problems goes away if these functions just take the timeout by value?
libc++ implementers have strong preference just changing the API to take these arguments by value, and it is not an ABI break for us as the function signature has changed.
Proposed resolution:
This wording is relative to this CD preview draft.
Modify 32.7.4 [thread.condition.condvar] as indicated:
[…]namespace std { class condition_variable { public: […] template<class Predicate> void wait(unique_lock<mutex>& lock, Predicate pred); template<class Clock, class Duration> cv_status wait_until(unique_lock<mutex>& lock,constchrono::time_point<Clock, Duration>&abs_time); template<class Clock, class Duration, class Predicate> bool wait_until(unique_lock<mutex>& lock,constchrono::time_point<Clock, Duration>&abs_time, Predicate pred); template<class Rep, class Period> cv_status wait_for(unique_lock<mutex>& lock,constchrono::duration<Rep, Period>&rel_time); template<class Rep, class Period, class Predicate> bool wait_for(unique_lock<mutex>& lock,constchrono::duration<Rep, Period>&rel_time, Predicate pred); […] }; }template<class Clock, class Duration> cv_status wait_until(unique_lock<mutex>& lock,constchrono::time_point<Clock, Duration>&abs_time);-17- Preconditions: […]
[…]template<class Rep, class Period> cv_status wait_for(unique_lock<mutex>& lock,constchrono::duration<Rep, Period>&rel_time);-23- Preconditions: […]
[…]template<class Clock, class Duration, class Predicate> bool wait_until(unique_lock<mutex>& lock,constchrono::time_point<Clock, Duration>&abs_time, Predicate pred);-29- Preconditions: […]
[…]template<class Rep, class Period, class Predicate> bool wait_for(unique_lock<mutex>& lock,constchrono::duration<Rep, Period>&rel_time, Predicate pred);-35- Preconditions: […]
[…]
Modify 32.7.5.1 [thread.condition.condvarany.general] as indicated:
namespace std { class condition_variable_any { public: […] // 32.7.5.2 [thread.condvarany.wait], noninterruptible waits template<class Lock> void wait(Lock& lock); template<class Lock, class Predicate> void wait(Lock& lock, Predicate pred); template<class Lock, class Clock, class Duration> cv_status wait_until(Lock& lock,constchrono::time_point<Clock, Duration>&abs_time); template<class Lock, class Clock, class Duration, class Predicate> bool wait_until(Lock& lock,constchrono::time_point<Clock, Duration>&abs_time, Predicate pred); template<class Lock, class Rep, class Period> cv_status wait_for(Lock& lock,constchrono::duration<Rep, Period>&rel_time); template<class Lock, class Rep, class Period, class Predicate> bool wait_for(Lock& lock,constchrono::duration<Rep, Period>&rel_time, Predicate pred); // 32.7.5.3 [thread.condvarany.intwait], interruptible waits template<class Lock, class Predicate> bool wait(Lock& lock, stop_token stoken, Predicate pred); template<class Lock, class Clock, class Duration, class Predicate> bool wait_until(Lock& lock, stop_token stoken,constchrono::time_point<Clock, Duration>&abs_time, Predicate pred); template<class Lock, class Rep, class Period, class Predicate> bool wait_for(Lock& lock, stop_token stoken,constchrono::duration<Rep, Period>&rel_time, Predicate pred); }; }
Modify 32.7.5.2 [thread.condvarany.wait] as indicated:
[…]template<class Lock, class Clock, class Duration> cv_status wait_until(Lock& lock,constchrono::time_point<Clock, Duration>&abs_time);-6- Effects: […]
[…]template<class Lock, class Rep, class Period> cv_status wait_for(Lock& lock,constchrono::duration<Rep, Period>&rel_time);-11- Effects: […]
[…]template<class Lock, class Clock, class Duration, class Predicate> bool wait_until(Lock& lock,constchrono::time_point<Clock, Duration>&abs_time, Predicate pred);-16- Effects: […]
[…]template<class Lock, class Rep, class Period, class Predicate> bool wait_for(Lock& lock,constchrono::duration<Rep, Period>&rel_time, Predicate pred);-19- Effects: […]
Modify 32.7.5.3 [thread.condvarany.intwait] as indicated:
[…]template<class Lock, class Clock, class Duration, class Predicate> bool wait_until(Lock& lock, stop_token stoken,constchrono::time_point<Clock, Duration>&abs_time, Predicate pred);-7- Effects: […]
[…]template<class Lock, class Rep, class Period, class Predicate> bool wait_for(Lock& lock, stop_token stoken,constchrono::duration<Rep, Period>&rel_time, Predicate pred);-13- Effects: […]