Title
`condition_variable{_any}::wait_{for, until}` should take timeout by value
Status
ready
Section
[thread.condition.condvar][thread.condition.condvarany]
Submitter
Hui Xie

Created on 2025-07-19.00:00:00 last changed yesterday

Messages

Date: 2025-08-29.11:28:59

Proposed resolution:

This wording is relative to N5014.

  1. 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,
                               const chrono::time_point<Clock, Duration>& abs_time);
        template<class Clock, class Duration, class Predicate>
          bool wait_until(unique_lock<mutex>& lock,
                          const chrono::time_point<Clock, Duration>& abs_time,
                          Predicate pred);
        template<class Rep, class Period>
          cv_status wait_for(unique_lock<mutex>& lock,
                             const chrono::duration<Rep, Period>& rel_time);
        template<class Rep, class Period, class Predicate>
          bool wait_for(unique_lock<mutex>& lock,
                        const chrono::duration<Rep, Period>& rel_time,
                        Predicate pred);    
        […]
      };
    }
    
    […]
    template<class Clock, class Duration>
      cv_status wait_until(unique_lock<mutex>& lock,
                           const chrono::time_point<Clock, Duration>& abs_time);
    

    -17- Preconditions: […]

    […]

    template<class Rep, class Period>
      cv_status wait_for(unique_lock<mutex>& lock,
                         const chrono::duration<Rep, Period>& rel_time);
    

    -23- Preconditions: […]

    […]

    template<class Clock, class Duration, class Predicate>
      bool wait_until(unique_lock<mutex>& lock,
                      const chrono::time_point<Clock, Duration>& abs_time,
                      Predicate pred);
    

    -29- Preconditions: […]

    […]

    template<class Rep, class Period, class Predicate>
      bool wait_for(unique_lock<mutex>& lock,
                    const chrono::duration<Rep, Period>& rel_time,
                    Predicate pred);    
    

    -35- Preconditions: […]

    […]

  2. 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, const chrono::time_point<Clock, Duration>& abs_time);
        template<class Lock, class Clock, class Duration, class Predicate>
          bool wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time,
                          Predicate pred);
        template<class Lock, class Rep, class Period>
          cv_status wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time);
        template<class Lock, class Rep, class Period, class Predicate>
          bool wait_for(Lock& lock, const chrono::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,
                          const chrono::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,
                        const chrono::duration<Rep, Period>& rel_time, Predicate pred);
      };
    }
    
  3. Modify [thread.condvarany.wait] as indicated:

    […]
    template<class Lock, class Clock, class Duration>
      cv_status wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time);
    

    -6- Effects: […]

    […]

    template<class Lock, class Rep, class Period>
      cv_status wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time);
    

    -11- Effects: […]

    […]

    template<class Lock, class Clock, class Duration, class Predicate>
      bool wait_until(Lock& lock, const chrono::time_point<Clock, Duration>& abs_time,
                      Predicate pred);
    

    -16- Effects: […]

    […]

    template<class Lock, class Rep, class Period, class Predicate>
      bool wait_for(Lock& lock, const chrono::duration<Rep, Period>& rel_time, Predicate pred);
    

    -19- Effects: […]

  4. Modify [thread.condvarany.intwait] as indicated:

    […]
    template<class Lock, class Clock, class Duration, class Predicate>
      bool wait_until(Lock& lock, stop_token stoken,
                      const chrono::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,
                    const chrono::duration<Rep, Period>& rel_time, Predicate pred);
    

    -13- Effects: […]

Date: 2025-08-15.00:00:00

[ 2025-08-29; Reflector poll ]

Set status to Tentatively Ready after nine votes in favour during reflector poll.

Date: 2025-07-19.00:00:00

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 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 .

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:59adminsetmessages: + msg14973
2025-08-29 11:28:59adminsetstatus: new -> ready
2025-07-26 17:41:59adminsetmessages: + msg14909
2025-07-19 00:00:00admincreate