Created on 2025-07-19.00:00:00 last changed yesterday
Proposed resolution:
This wording is relative to N5014.
Modify [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 [thread.condition.condvarany.general] as indicated:
namespace std { class condition_variable_any { public: […] // [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); // [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 [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 [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: […]
[ 2025-08-29; Reflector poll ]
Set status to Tentatively Ready after nine votes in favour during reflector poll.
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`.
Another example related to this issue: We (libc++) recently received a bug report on `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.
For `wait_for`, the spec hasEffects: 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 .
For `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.
History | |||
---|---|---|---|
Date | User | Action | Args |
2025-08-29 11:28:59 | admin | set | messages: + msg14973 |
2025-08-29 11:28:59 | admin | set | status: new -> ready |
2025-07-26 17:41:59 | admin | set | messages: + msg14909 |
2025-07-19 00:00:00 | admin | create |