Title
condition_variable::wait_for is overspecified
Status
new
Section
[thread.condition.condvar]
Submitter
Jonathan Wakely

Created on 2020-11-18.00:00:00 last changed 5 months ago

Messages

Date: 2024-06-18.11:41:46

Proposed resolution:

This wording is relative to N4981.

  1. Modify [thread.condition.general] as indicated, adding a new paragraph to the end:

    -6- The definitions in [thread.condition] make use of the following exposition-only function:
    template<class Dur>
      chrono::steady_clock::time_point rel-to-abs(const Dur& rel_time)
      { return chrono::steady_clock::now() + chrono::ceil<chrono::steady_clock::duration>(rel_time); }
    
    
  2. Modify [thread.condition.condvar] as indicated:

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

    -23- Preconditions: `lock.owns_lock()` is `true` and `lock.mutex()` is locked by the calling thread, and either [...]

    -24- Effects: Equivalent to:

    return wait_until(lock, chrono::steady_clock::now() + rel_time rel-to-abs(rel_time));

    [...]

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

    -35- Preconditions: `lock.owns_lock()` is `true` and `lock.mutex()` is locked by the calling thread, and either [...]

    -36- Effects: Equivalent to:

    return wait_until(lock, chrono::steady_clock::now() + rel_time rel-to-abs(rel_time), std::move(pred));

    [Note 8: There is no blocking if `pred()` is initially `true`, even if the timeout has already expired. — end note]

  3. Modify [thread.condvarany.wait] as indicated:

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

    -11- Effects: Equivalent to:

    return wait_until(lock, chrono::steady_clock::now() + rel_time rel-to-abs(rel_time));

    [...]

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

    -19- Effects: Equivalent to:

    return wait_until(lock, chrono::steady_clock::now() + rel_time rel-to-abs(rel_time), std::move(pred));

Date: 2024-06-15.00:00:00

[ 2024-06-18; Jonathan adds wording ]

Date: 2020-11-15.00:00:00

[ 2020-11-29; Reflector prioritization ]

Set priority to 3 during reflector discussions.

Date: 2020-11-18.00:00:00

[thread.condition.condvar] p24 says:

Effects: Equivalent to: return wait_until(lock, chrono::steady_clock::now() + rel_time);

This is overspecification, removing implementer freedom to make cv.wait_for(duration<float>(1)) work accurately.

The type of steady_clock::now() + duration<float>(n) is time_point<steady_clock, duration<float, steady_clock::period>>. If the steady clock's period is std::nano and its epoch is the time the system booted, then in under a second a 32-bit float becomes unable to exactly represent those time_points! Every second after boot makes duration<float, nano> less precise.

This means that adding a duration<float> to a time_point (or duration) measured in nanoseconds is unlikely to produce an accurate value. Either it will round down to a value less than now(), or round up to one greater than now() + 1s. Either way, the wait_for(rel_time) doesn't wait for the specified time, and users think the implementation is faulty.

A possible solution is to use steady_clock::now() + ceil<steady_clock::duration>(rel_time) instead. This converts the relative time to a suitably large integer, and then the addition is not affected by floating-point rounding errors due to the limited precision of 32-bit float. Libstdc++ has been doing this for nearly three years. Alternatively, the standard could just say that the relative timeout is converted to an absolute timeout measured against the steady clock, and leave the details to the implementation. Some implementations might not be affected by the problem (e.g. if the steady clock measures milliseconds, or processes only run for a few seconds and use the process start as the steady clock's epoch).

This also affects the other overload of condition_variable::wait_for, and both overloads of condition_variable_any::wait_for.

History
Date User Action Args
2024-06-18 11:41:46adminsetmessages: + msg14168
2024-06-18 11:41:46adminsetmessages: + msg14167
2020-11-29 13:57:16adminsetmessages: + msg11635
2020-11-18 00:00:00admincreate