Skip to content

Commit e93122f

Browse files
committed
New issue from Hui Xie: "condition_variable{_any}::wait_{for, until} should take timeout by value"
1 parent 5bcc7bb commit e93122f

File tree

1 file changed

+324
-0
lines changed

1 file changed

+324
-0
lines changed

xml/issue4301.xml

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
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&lt;Task, vector&lt;Task&gt;, CompareDeadlines&gt; 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+
[&hellip;]
128+
template&lt;class Predicate&gt;
129+
void wait(unique_lock&lt;mutex&gt;&amp; lock, Predicate pred);
130+
template&lt;class Clock, class Duration&gt;
131+
cv_status wait_until(unique_lock&lt;mutex&gt;&amp; lock,
132+
<del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time);
133+
template&lt;class Clock, class Duration, class Predicate&gt;
134+
bool wait_until(unique_lock&lt;mutex&gt;&amp; lock,
135+
<del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time,
136+
Predicate pred);
137+
template&lt;class Rep, class Period&gt;
138+
cv_status wait_for(unique_lock&lt;mutex&gt;&amp; lock,
139+
<del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time);
140+
template&lt;class Rep, class Period, class Predicate&gt;
141+
bool wait_for(unique_lock&lt;mutex&gt;&amp; lock,
142+
<del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time,
143+
Predicate pred);
144+
[&hellip;]
145+
};
146+
}
147+
</pre>
148+
</blockquote>
149+
[&hellip;]
150+
<pre>
151+
template&lt;class Clock, class Duration&gt;
152+
cv_status wait_until(unique_lock&lt;mutex&gt;&amp; lock,
153+
<del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time);
154+
</pre>
155+
<blockquote>
156+
<p>
157+
-17- <i>Preconditions</i>: [&hellip;]
158+
<p/>
159+
[&hellip;]
160+
</p>
161+
</blockquote>
162+
<pre>
163+
template&lt;class Rep, class Period&gt;
164+
cv_status wait_for(unique_lock&lt;mutex&gt;&amp; lock,
165+
<del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time);
166+
</pre>
167+
<blockquote>
168+
<p>
169+
-23- <i>Preconditions</i>: [&hellip;]
170+
<p/>
171+
[&hellip;]
172+
</p>
173+
</blockquote>
174+
<pre>
175+
template&lt;class Clock, class Duration, class Predicate&gt;
176+
bool wait_until(unique_lock&lt;mutex&gt;&amp; lock,
177+
<del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time,
178+
Predicate pred);
179+
</pre>
180+
<blockquote>
181+
<p>
182+
-29- <i>Preconditions</i>: [&hellip;]
183+
<p/>
184+
[&hellip;]
185+
</p>
186+
</blockquote>
187+
<pre>
188+
template&lt;class Rep, class Period, class Predicate&gt;
189+
bool wait_for(unique_lock&lt;mutex&gt;&amp; lock,
190+
<del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time,
191+
Predicate pred);
192+
</pre>
193+
<blockquote>
194+
<p>
195+
-35- <i>Preconditions</i>: [&hellip;]
196+
<p/>
197+
[&hellip;]
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+
[&hellip;]
211+
// <i><sref ref="[thread.condvarany.wait]"/>, noninterruptible waits</i>
212+
template&lt;class Lock&gt;
213+
void wait(Lock&amp; lock);
214+
template&lt;class Lock, class Predicate&gt;
215+
void wait(Lock&amp; lock, Predicate pred);
216+
217+
template&lt;class Lock, class Clock, class Duration&gt;
218+
cv_status wait_until(Lock&amp; lock, <del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time);
219+
template&lt;class Lock, class Clock, class Duration, class Predicate&gt;
220+
bool wait_until(Lock&amp; lock, <del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time,
221+
Predicate pred);
222+
template&lt;class Lock, class Rep, class Period&gt;
223+
cv_status wait_for(Lock&amp; lock, <del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time);
224+
template&lt;class Lock, class Rep, class Period, class Predicate&gt;
225+
bool wait_for(Lock&amp; lock, <del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time, Predicate pred);
226+
227+
// <i><sref ref="[thread.condvarany.intwait]"/>, interruptible waits</i>
228+
template&lt;class Lock, class Predicate&gt;
229+
bool wait(Lock&amp; lock, stop_token stoken, Predicate pred);
230+
template&lt;class Lock, class Clock, class Duration, class Predicate&gt;
231+
bool wait_until(Lock&amp; lock, stop_token stoken,
232+
<del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time, Predicate pred);
233+
template&lt;class Lock, class Rep, class Period, class Predicate&gt;
234+
bool wait_for(Lock&amp; lock, stop_token stoken,
235+
<del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</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+
[&hellip;]
246+
<pre>
247+
template&lt;class Lock, class Clock, class Duration&gt;
248+
cv_status wait_until(Lock&amp; lock, <del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time);
249+
</pre>
250+
<blockquote>
251+
<p>
252+
-6- <i>Effects</i>: [&hellip;]
253+
<p/>
254+
[&hellip;]
255+
</p>
256+
</blockquote>
257+
<pre>
258+
template&lt;class Lock, class Rep, class Period&gt;
259+
cv_status wait_for(Lock&amp; lock, <del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time);
260+
</pre>
261+
<blockquote>
262+
<p>
263+
-11- <i>Effects</i>: [&hellip;]
264+
<p/>
265+
[&hellip;]
266+
</p>
267+
</blockquote>
268+
<pre>
269+
template&lt;class Lock, class Clock, class Duration, class Predicate&gt;
270+
bool wait_until(Lock&amp; lock, <del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time,
271+
Predicate pred);
272+
</pre>
273+
<blockquote>
274+
<p>
275+
-16- <i>Effects</i>: [&hellip;]
276+
<p/>
277+
[&hellip;]
278+
</p>
279+
</blockquote>
280+
<pre>
281+
template&lt;class Lock, class Rep, class Period, class Predicate&gt;
282+
bool wait_for(Lock&amp; lock, <del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time, Predicate pred);
283+
</pre>
284+
<blockquote>
285+
<p>
286+
-19- <i>Effects</i>: [&hellip;]
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+
[&hellip;]
296+
<pre>
297+
template&lt;class Lock, class Clock, class Duration, class Predicate&gt;
298+
bool wait_until(Lock&amp; lock, stop_token stoken,
299+
<del>const</del> chrono::time_point&lt;Clock, Duration&gt;<del>&amp;</del> abs_time, Predicate pred);
300+
</pre>
301+
<blockquote>
302+
<p>
303+
-7- <i>Effects</i>: [&hellip;]
304+
<p/>
305+
[&hellip;]
306+
</p>
307+
</blockquote>
308+
<pre>
309+
template&lt;class Lock, class Rep, class Period, class Predicate&gt;
310+
bool wait_for(Lock&amp; lock, stop_token stoken,
311+
<del>const</del> chrono::duration&lt;Rep, Period&gt;<del>&amp;</del> rel_time, Predicate pred);
312+
</pre>
313+
<blockquote>
314+
<p>
315+
-13- <i>Effects</i>: [&hellip;]
316+
</p>
317+
</blockquote>
318+
</blockquote>
319+
</li>
320+
321+
</ol>
322+
</resolution>
323+
324+
</issue>

0 commit comments

Comments
 (0)