|
| 1 | +<?xml version='1.0' encoding='utf-8' standalone='no'?> |
| 2 | +<!DOCTYPE issue SYSTEM "lwg-issue.dtd"> |
| 3 | + |
| 4 | +<issue num="4301" status="New"> |
| 5 | +<title>`condition_variable{_any}::wait_{for, until}` should take timeout by value</title> |
| 6 | +<section><sref ref="[thread.condition.condvar]"/><sref ref="[thread.condition.condvarany]"/></section> |
| 7 | +<submitter>Hui Xie</submitter> |
| 8 | +<date>19 Jul 2025</date> |
| 9 | +<priority>99</priority> |
| 10 | + |
| 11 | +<discussion> |
| 12 | +<p> |
| 13 | +At the moment, both `condition_variable` and `condition_variable_any`'s |
| 14 | +`wait_for` and `wait_until` take the timeout `time_point`/`duration` by |
| 15 | +`const` reference. This can cause surprising behaviour. Given the following |
| 16 | +example (thanks to Tim Song): |
| 17 | +</p> |
| 18 | +<blockquote><pre> |
| 19 | +struct Task { |
| 20 | + system_clock::time_point deadline; |
| 21 | + // stuff |
| 22 | +}; |
| 23 | + |
| 24 | +std::mutex mtx; |
| 25 | +std::condition_variable cv; |
| 26 | +std::priority_queue<Task, vector<Task>, CompareDeadlines> queue; |
| 27 | + |
| 28 | +// thread 1 |
| 29 | +std::unique_lock lck(mtx); |
| 30 | +if (queue.empty()) { cv.wait(lck); } |
| 31 | +else { cv.wait_until(lck, queue.top().deadline); } |
| 32 | + |
| 33 | +// thread 2 |
| 34 | +std::lock_guard lck(mtx); |
| 35 | +queue.push(/* some task */); |
| 36 | +cv.notify_one(); |
| 37 | +</pre></blockquote> |
| 38 | +<p> |
| 39 | +From the user's point of view, it is sufficiently locked on both threads. However, |
| 40 | +due to the fact that the `time_point` is taken by reference, and that both libc++ |
| 41 | +and libstdc++'s implementation will read the value again after waking up, this |
| 42 | +will read a dangling reference of the `time_point`. |
| 43 | +<p/> |
| 44 | +Another example related to this issue: |
| 45 | +<p/> |
| 46 | +We (libc++) recently received a bug report on `condition_variable{_any}::wait_{for, until}`. |
| 47 | +<p/> |
| 48 | +Basically the user claims that these functions take `time_point`/`duration` by `const` |
| 49 | +reference, if the user modifies the `time_point`/`duration` on another thread with |
| 50 | +the same mutex, they can get unexpected return value for `condition_variable`, and |
| 51 | +data race for `conditional_variable_any`. |
| 52 | +<p/> |
| 53 | +Bug report <a href="https://github.com/llvm/llvm-project/pull/148330#issuecomment-3065062889">here</a>. |
| 54 | +<p/> |
| 55 | +Reproducer (libstdc++ has the same behaviour as ours) <a href="https://godbolt.org/z/GnY35T3hn">on godbolt</a>. |
| 56 | +</p> |
| 57 | +<blockquote><pre> |
| 58 | +std::mutex mutex; |
| 59 | +std::condition_variable cv; |
| 60 | +auto timeout = std::chrono::steady_clock::time_point::max(); |
| 61 | + |
| 62 | +// Thread 1: |
| 63 | +std::unique_lock lock(mutex); |
| 64 | +const auto status = cv.wait_until(lock, timeout); |
| 65 | + |
| 66 | +// Thread 2: |
| 67 | +std::unique_lock lock(mutex); |
| 68 | +cv.notify_one(); |
| 69 | +timeout = std::chrono::steady_clock::time_point::min(); |
| 70 | +</pre></blockquote> |
| 71 | +<p> |
| 72 | +So basically the problem was that when we return whether there is `no_timeout` or `timeout` |
| 73 | +at the end of the function, we read the `const` reference again, which can be changed since |
| 74 | +the beginning of the function. For `condition_variable`, it is "unexpected results" according |
| 75 | +to the user. And in `conditional_variable_any`, we actually unlock the user lock and acquire |
| 76 | +our internal lock, then read the input again, so this is actually a data race. |
| 77 | +<p/> |
| 78 | +For `wait_for`, the spec has |
| 79 | +</p> |
| 80 | +<blockquote> |
| 81 | +<p> |
| 82 | +<i>Effects</i>: Equivalent to: <tt>return wait_until(lock, chrono::steady_clock::now() + rel_time);</tt> |
| 83 | +</p> |
| 84 | +</blockquote> |
| 85 | +<p> |
| 86 | +So the user can claim our implementation is not conforming because the spec says there needs |
| 87 | +to be a temporary `time_point` (`now + duration`) created and since it should operate on this |
| 88 | +temporary `time_point`. There shouldn't be any unexpected behaviour or data race . |
| 89 | +<p/> |
| 90 | +For `wait_until` it is unclear whether the spec has implications that implementations are allowed |
| 91 | +to read `abs_time` while the user's lock is unlocked. |
| 92 | +<p/> |
| 93 | +it is also unclear if an implementation is allowed to return `timeout` if `cv` indeed does |
| 94 | +not wait longer than the original value of `timeout`. If it is not allowed, implementations |
| 95 | +will have to make a local copy of the input `rel_time` or `abs_time`, which defeats the purpose |
| 96 | +of taking arguments by `const` reference. |
| 97 | +<p/> |
| 98 | +For both of the examples, Ville has a great comment in the reflector: |
| 99 | +</p> |
| 100 | +<blockquote style="border-left: 3px solid #ccc;padding-left: 15px;"> |
| 101 | +<p> |
| 102 | +It seems like a whole bag of problems goes away if these functions just take the timeout by value? |
| 103 | +</p> |
| 104 | +</blockquote> |
| 105 | +<p> |
| 106 | +libc++ implementers have strong preference just changing the API to take these arguments by value, |
| 107 | +and it is not an ABI break for us as the function signature has changed. |
| 108 | +</p> |
| 109 | +</discussion> |
| 110 | + |
| 111 | +<resolution> |
| 112 | +<p> |
| 113 | +This wording is relative to this |
| 114 | +<a href="https://github.com/cplusplus/draft/actions/runs/16433597877/artifacts/3583518547">CD preview draft</a>. |
| 115 | +</p> |
| 116 | + |
| 117 | +<ol> |
| 118 | + |
| 119 | +<li><p>Modify <sref ref="[thread.condition.condvar]"/> as indicated:</p> |
| 120 | + |
| 121 | +<blockquote> |
| 122 | +<blockquote> |
| 123 | +<pre> |
| 124 | +namespace std { |
| 125 | + class condition_variable { |
| 126 | + public: |
| 127 | + […] |
| 128 | + template<class Predicate> |
| 129 | + void wait(unique_lock<mutex>& lock, Predicate pred); |
| 130 | + template<class Clock, class Duration> |
| 131 | + cv_status wait_until(unique_lock<mutex>& lock, |
| 132 | + <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time); |
| 133 | + template<class Clock, class Duration, class Predicate> |
| 134 | + bool wait_until(unique_lock<mutex>& lock, |
| 135 | + <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time, |
| 136 | + Predicate pred); |
| 137 | + template<class Rep, class Period> |
| 138 | + cv_status wait_for(unique_lock<mutex>& lock, |
| 139 | + <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time); |
| 140 | + template<class Rep, class Period, class Predicate> |
| 141 | + bool wait_for(unique_lock<mutex>& lock, |
| 142 | + <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time, |
| 143 | + Predicate pred); |
| 144 | + […] |
| 145 | + }; |
| 146 | +} |
| 147 | +</pre> |
| 148 | +</blockquote> |
| 149 | +[…] |
| 150 | +<pre> |
| 151 | +template<class Clock, class Duration> |
| 152 | + cv_status wait_until(unique_lock<mutex>& lock, |
| 153 | + <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time); |
| 154 | +</pre> |
| 155 | +<blockquote> |
| 156 | +<p> |
| 157 | +-17- <i>Preconditions</i>: […] |
| 158 | +<p/> |
| 159 | +[…] |
| 160 | +</p> |
| 161 | +</blockquote> |
| 162 | +<pre> |
| 163 | +template<class Rep, class Period> |
| 164 | + cv_status wait_for(unique_lock<mutex>& lock, |
| 165 | + <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time); |
| 166 | +</pre> |
| 167 | +<blockquote> |
| 168 | +<p> |
| 169 | +-23- <i>Preconditions</i>: […] |
| 170 | +<p/> |
| 171 | +[…] |
| 172 | +</p> |
| 173 | +</blockquote> |
| 174 | +<pre> |
| 175 | +template<class Clock, class Duration, class Predicate> |
| 176 | + bool wait_until(unique_lock<mutex>& lock, |
| 177 | + <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time, |
| 178 | + Predicate pred); |
| 179 | +</pre> |
| 180 | +<blockquote> |
| 181 | +<p> |
| 182 | +-29- <i>Preconditions</i>: […] |
| 183 | +<p/> |
| 184 | +[…] |
| 185 | +</p> |
| 186 | +</blockquote> |
| 187 | +<pre> |
| 188 | +template<class Rep, class Period, class Predicate> |
| 189 | + bool wait_for(unique_lock<mutex>& lock, |
| 190 | + <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time, |
| 191 | + Predicate pred); |
| 192 | +</pre> |
| 193 | +<blockquote> |
| 194 | +<p> |
| 195 | +-35- <i>Preconditions</i>: […] |
| 196 | +<p/> |
| 197 | +[…] |
| 198 | +</p> |
| 199 | +</blockquote> |
| 200 | +</blockquote> |
| 201 | +</li> |
| 202 | + |
| 203 | +<li><p>Modify <sref ref="[thread.condition.condvarany.general]"/> as indicated:</p> |
| 204 | + |
| 205 | +<blockquote> |
| 206 | +<pre> |
| 207 | +namespace std { |
| 208 | + class condition_variable_any { |
| 209 | + public: |
| 210 | + […] |
| 211 | + // <i><sref ref="[thread.condvarany.wait]"/>, noninterruptible waits</i> |
| 212 | + template<class Lock> |
| 213 | + void wait(Lock& lock); |
| 214 | + template<class Lock, class Predicate> |
| 215 | + void wait(Lock& lock, Predicate pred); |
| 216 | + |
| 217 | + template<class Lock, class Clock, class Duration> |
| 218 | + cv_status wait_until(Lock& lock, <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time); |
| 219 | + template<class Lock, class Clock, class Duration, class Predicate> |
| 220 | + bool wait_until(Lock& lock, <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time, |
| 221 | + Predicate pred); |
| 222 | + template<class Lock, class Rep, class Period> |
| 223 | + cv_status wait_for(Lock& lock, <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time); |
| 224 | + template<class Lock, class Rep, class Period, class Predicate> |
| 225 | + bool wait_for(Lock& lock, <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time, Predicate pred); |
| 226 | + |
| 227 | + // <i><sref ref="[thread.condvarany.intwait]"/>, interruptible waits</i> |
| 228 | + template<class Lock, class Predicate> |
| 229 | + bool wait(Lock& lock, stop_token stoken, Predicate pred); |
| 230 | + template<class Lock, class Clock, class Duration, class Predicate> |
| 231 | + bool wait_until(Lock& lock, stop_token stoken, |
| 232 | + <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time, Predicate pred); |
| 233 | + template<class Lock, class Rep, class Period, class Predicate> |
| 234 | + bool wait_for(Lock& lock, stop_token stoken, |
| 235 | + <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time, Predicate pred); |
| 236 | + }; |
| 237 | +} |
| 238 | +</pre> |
| 239 | +</blockquote> |
| 240 | +</li> |
| 241 | + |
| 242 | +<li><p>Modify <sref ref="[thread.condvarany.wait]"/> as indicated:</p> |
| 243 | + |
| 244 | +<blockquote> |
| 245 | +[…] |
| 246 | +<pre> |
| 247 | +template<class Lock, class Clock, class Duration> |
| 248 | + cv_status wait_until(Lock& lock, <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time); |
| 249 | +</pre> |
| 250 | +<blockquote> |
| 251 | +<p> |
| 252 | +-6- <i>Effects</i>: […] |
| 253 | +<p/> |
| 254 | +[…] |
| 255 | +</p> |
| 256 | +</blockquote> |
| 257 | +<pre> |
| 258 | +template<class Lock, class Rep, class Period> |
| 259 | + cv_status wait_for(Lock& lock, <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time); |
| 260 | +</pre> |
| 261 | +<blockquote> |
| 262 | +<p> |
| 263 | +-11- <i>Effects</i>: […] |
| 264 | +<p/> |
| 265 | +[…] |
| 266 | +</p> |
| 267 | +</blockquote> |
| 268 | +<pre> |
| 269 | +template<class Lock, class Clock, class Duration, class Predicate> |
| 270 | + bool wait_until(Lock& lock, <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time, |
| 271 | + Predicate pred); |
| 272 | +</pre> |
| 273 | +<blockquote> |
| 274 | +<p> |
| 275 | +-16- <i>Effects</i>: […] |
| 276 | +<p/> |
| 277 | +[…] |
| 278 | +</p> |
| 279 | +</blockquote> |
| 280 | +<pre> |
| 281 | +template<class Lock, class Rep, class Period, class Predicate> |
| 282 | + bool wait_for(Lock& lock, <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time, Predicate pred); |
| 283 | +</pre> |
| 284 | +<blockquote> |
| 285 | +<p> |
| 286 | +-19- <i>Effects</i>: […] |
| 287 | +</p> |
| 288 | +</blockquote> |
| 289 | +</blockquote> |
| 290 | +</li> |
| 291 | + |
| 292 | +<li><p>Modify <sref ref="[thread.condvarany.intwait]"/> as indicated:</p> |
| 293 | + |
| 294 | +<blockquote> |
| 295 | +[…] |
| 296 | +<pre> |
| 297 | +template<class Lock, class Clock, class Duration, class Predicate> |
| 298 | + bool wait_until(Lock& lock, stop_token stoken, |
| 299 | + <del>const</del> chrono::time_point<Clock, Duration><del>&</del> abs_time, Predicate pred); |
| 300 | +</pre> |
| 301 | +<blockquote> |
| 302 | +<p> |
| 303 | +-7- <i>Effects</i>: […] |
| 304 | +<p/> |
| 305 | +[…] |
| 306 | +</p> |
| 307 | +</blockquote> |
| 308 | +<pre> |
| 309 | +template<class Lock, class Rep, class Period, class Predicate> |
| 310 | + bool wait_for(Lock& lock, stop_token stoken, |
| 311 | + <del>const</del> chrono::duration<Rep, Period><del>&</del> rel_time, Predicate pred); |
| 312 | +</pre> |
| 313 | +<blockquote> |
| 314 | +<p> |
| 315 | +-13- <i>Effects</i>: […] |
| 316 | +</p> |
| 317 | +</blockquote> |
| 318 | +</blockquote> |
| 319 | +</li> |
| 320 | + |
| 321 | +</ol> |
| 322 | +</resolution> |
| 323 | + |
| 324 | +</issue> |
0 commit comments