Skip to content

Commit 85a8e5c

Browse files
authored
[libc++] fix condition_variable_any hangs on stop_request (#77127)
When I implemented `condition_variable_any::wait`, I missed the most important paragraph in the spec: > The following wait functions will be notified when there is a stop request on the passed stop_token. > In that case the functions return immediately, returning false if the predicate evaluates to false. From https://eel.is/c++draft/thread.condition#thread.condvarany.intwait-1. Fixes #76807
1 parent 1cc7cd4 commit 85a8e5c

File tree

5 files changed

+311
-30
lines changed

5 files changed

+311
-30
lines changed

libcxx/include/condition_variable

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ public:
126126
#include <__condition_variable/condition_variable.h>
127127
#include <__config>
128128
#include <__memory/shared_ptr.h>
129-
#include <__memory/unique_ptr.h>
130129
#include <__mutex/lock_guard.h>
131130
#include <__mutex/mutex.h>
132131
#include <__mutex/tag_types.h>
133132
#include <__mutex/unique_lock.h>
133+
#include <__stop_token/stop_callback.h>
134134
#include <__stop_token/stop_token.h>
135135
#include <__utility/move.h>
136136
#include <version>
@@ -200,19 +200,26 @@ inline void condition_variable_any::notify_all() _NOEXCEPT {
200200
__cv_.notify_all();
201201
}
202202

203-
struct __lock_external {
204-
template <class _Lock>
205-
_LIBCPP_HIDE_FROM_ABI void operator()(_Lock* __m) {
206-
__m->lock();
203+
template <class _Lock>
204+
struct __unlock_guard {
205+
_Lock& __lock_;
206+
207+
_LIBCPP_HIDE_FROM_ABI __unlock_guard(_Lock& __lock) : __lock_(__lock) { __lock_.unlock(); }
208+
209+
_LIBCPP_HIDE_FROM_ABI ~__unlock_guard() _NOEXCEPT // turns exception to std::terminate
210+
{
211+
__lock_.lock();
207212
}
213+
214+
__unlock_guard(const __unlock_guard&) = delete;
215+
__unlock_guard& operator=(const __unlock_guard&) = delete;
208216
};
209217

210218
template <class _Lock>
211219
void condition_variable_any::wait(_Lock& __lock) {
212220
shared_ptr<mutex> __mut = __mut_;
213221
unique_lock<mutex> __lk(*__mut);
214-
__lock.unlock();
215-
unique_ptr<_Lock, __lock_external> __lxx(&__lock);
222+
__unlock_guard<_Lock> __unlock(__lock);
216223
lock_guard<unique_lock<mutex> > __lx(__lk, adopt_lock_t());
217224
__cv_.wait(__lk);
218225
} // __mut_.unlock(), __lock.lock()
@@ -227,8 +234,7 @@ template <class _Lock, class _Clock, class _Duration>
227234
cv_status condition_variable_any::wait_until(_Lock& __lock, const chrono::time_point<_Clock, _Duration>& __t) {
228235
shared_ptr<mutex> __mut = __mut_;
229236
unique_lock<mutex> __lk(*__mut);
230-
__lock.unlock();
231-
unique_ptr<_Lock, __lock_external> __lxx(&__lock);
237+
__unlock_guard<_Lock> __unlock(__lock);
232238
lock_guard<unique_lock<mutex> > __lx(__lk, adopt_lock_t());
233239
return __cv_.wait_until(__lk, __t);
234240
} // __mut_.unlock(), __lock.lock()
@@ -256,24 +262,75 @@ condition_variable_any::wait_for(_Lock& __lock, const chrono::duration<_Rep, _Pe
256262
# if _LIBCPP_STD_VER >= 20 && !defined(_LIBCPP_HAS_NO_EXPERIMENTAL_STOP_TOKEN)
257263

258264
template <class _Lock, class _Predicate>
259-
bool condition_variable_any::wait(_Lock& __lock, stop_token __stoken, _Predicate __pred) {
260-
while (!__stoken.stop_requested()) {
265+
bool condition_variable_any::wait(_Lock& __user_lock, stop_token __stoken, _Predicate __pred) {
266+
if (__stoken.stop_requested())
267+
return __pred();
268+
269+
// Per https://eel.is/c++draft/thread.condition.condvarany#general-note-2,
270+
// we do need to take a copy of the shared pointer __mut_
271+
// This ensures that a thread can call the destructor immediately after calling
272+
// notify_all, without waiting all the wait calls.
273+
// A thread can also safely call the destructor immediately after calling
274+
// request_stop, as the call to request_stop would evaluate the callback,
275+
// which accesses the internal condition variable, immediately on the same thread.
276+
// In this situation, it is OK even without copying a shared ownership the internal
277+
// condition variable. However, this needs the evaluation of stop_callback to
278+
// happen-before the destruction.
279+
// The spec only says "Only the notification to unblock the wait needs to happen
280+
// before destruction". To make this work, we need to copy the shared ownership of
281+
// the internal condition variable inside this function, which is not possible
282+
// with the current ABI.
283+
shared_ptr<mutex> __mut = __mut_;
284+
285+
stop_callback __cb(__stoken, [this] { notify_all(); });
286+
287+
while (true) {
261288
if (__pred())
262289
return true;
263-
wait(__lock);
264-
}
290+
291+
// We need to take the internal lock before checking stop_requested,
292+
// so that the notification cannot come in between the stop_requested
293+
// check and entering the wait.
294+
// Note that the stop_callback takes the same internal lock before notifying
295+
unique_lock<mutex> __internal_lock(*__mut);
296+
if (__stoken.stop_requested())
297+
break;
298+
299+
__unlock_guard<_Lock> __unlock(__user_lock);
300+
unique_lock<mutex> __internal_lock2(
301+
std::move(__internal_lock)); // switch unlock order between __internal_lock and __user_lock
302+
__cv_.wait(__internal_lock2);
303+
} // __internal_lock2.unlock(), __user_lock.lock()
265304
return __pred();
266305
}
267306

268307
template <class _Lock, class _Clock, class _Duration, class _Predicate>
269308
bool condition_variable_any::wait_until(
270-
_Lock& __lock, stop_token __stoken, const chrono::time_point<_Clock, _Duration>& __abs_time, _Predicate __pred) {
271-
while (!__stoken.stop_requested()) {
309+
_Lock& __user_lock,
310+
stop_token __stoken,
311+
const chrono::time_point<_Clock, _Duration>& __abs_time,
312+
_Predicate __pred) {
313+
if (__stoken.stop_requested())
314+
return __pred();
315+
316+
shared_ptr<mutex> __mut = __mut_;
317+
stop_callback __cb(__stoken, [this] { notify_all(); });
318+
319+
while (true) {
272320
if (__pred())
273321
return true;
274-
if (wait_until(__lock, __abs_time) == cv_status::timeout)
275-
return __pred();
276-
}
322+
323+
unique_lock<mutex> __internal_lock(*__mut);
324+
if (__stoken.stop_requested())
325+
break;
326+
327+
__unlock_guard<_Lock> __unlock(__user_lock);
328+
unique_lock<mutex> __internal_lock2(
329+
std::move(__internal_lock)); // switch unlock order between __internal_lock and __user_lock
330+
331+
if (__cv_.wait_until(__internal_lock2, __abs_time) == cv_status::timeout)
332+
break;
333+
} // __internal_lock2.unlock(), __user_lock.lock()
277334
return __pred();
278335
}
279336

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4+
// See https://llvm.org/LICENSE.txt for license information.
5+
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6+
//
7+
//===----------------------------------------------------------------------===//
8+
9+
#ifndef TEST_STD_THREAD_CONDITION_CONDVARANY_HELPERS_H
10+
#define TEST_STD_THREAD_CONDITION_CONDVARANY_HELPERS_H
11+
12+
#include <chrono>
13+
#include <cassert>
14+
15+
#include "test_macros.h"
16+
17+
#if TEST_STD_VER >= 17
18+
19+
// wait_for and wait_until function can exit via
20+
// - predicate is true
21+
// - timeout
22+
// - stop_requested
23+
// The return value only tells if the predicate is true
24+
// when the function exits, but it does not tell whether
25+
// it is timeout or stop_requested.
26+
//
27+
// ElapsedTimeCheck would fail the test if a test takes
28+
// longer than a duration. This is useful because we can
29+
// ensure that the wait_{for, until} function does not
30+
// wait until the timeout
31+
struct ElapsedTimeCheck {
32+
ElapsedTimeCheck(std::chrono::steady_clock::duration timeoutDuration)
33+
: timeout_(std::chrono::steady_clock::now() + timeoutDuration) {}
34+
35+
ElapsedTimeCheck(ElapsedTimeCheck&&) = delete;
36+
ElapsedTimeCheck& operator=(ElapsedTimeCheck&&) = delete;
37+
38+
~ElapsedTimeCheck() { assert(std::chrono::steady_clock::now() < timeout_); }
39+
40+
std::chrono::time_point<std::chrono::steady_clock> timeout_;
41+
};
42+
43+
#endif
44+
45+
#endif // TEST_STD_THREAD_CONDITION_CONDVARANY_HELPERS_H

libcxx/test/std/thread/thread.condition/thread.condition.condvarany/wait_for_token_pred.pass.cpp

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <stop_token>
3030
#include <thread>
3131

32+
#include "helpers.h"
3233
#include "make_test_thread.h"
3334
#include "test_macros.h"
3435

@@ -44,6 +45,8 @@ void test() {
4445
Lock lock{mutex};
4546
ss.request_stop();
4647

48+
ElapsedTimeCheck check(1min);
49+
4750
// [Note 4: The returned value indicates whether the predicate evaluated to true
4851
// regardless of whether the timeout was triggered or a stop request was made.]
4952
std::same_as<bool> auto r1 = cv.wait_for(lock, ss.get_token(), -1h, []() { return false; });
@@ -69,6 +72,8 @@ void test() {
6972
Mutex mutex;
7073
Lock lock{mutex};
7174

75+
ElapsedTimeCheck check(1min);
76+
7277
std::same_as<bool> auto r1 = cv.wait_for(lock, ss.get_token(), -1h, []() { return true; });
7378
assert(r1);
7479

@@ -83,6 +88,8 @@ void test() {
8388
Mutex mutex;
8489
Lock lock{mutex};
8590

91+
ElapsedTimeCheck check(1min);
92+
8693
std::same_as<bool> auto r1 = cv.wait_for(lock, ss.get_token(), -1h, []() { return false; });
8794
assert(!r1);
8895
}
@@ -117,6 +124,8 @@ void test() {
117124
cv.notify_all();
118125
});
119126

127+
ElapsedTimeCheck check(10min);
128+
120129
std::same_as<bool> auto r1 = cv.wait_for(lock, ss.get_token(), 1h, [&]() { return flag; });
121130
assert(flag);
122131
assert(r1);
@@ -143,6 +152,8 @@ void test() {
143152
}
144153
});
145154

155+
ElapsedTimeCheck check(10min);
156+
146157
std::same_as<bool> auto r = cv.wait_for(lock, ss.get_token(), 1h, [&]() {
147158
start.store(true);
148159
start.notify_all();
@@ -155,6 +166,60 @@ void test() {
155166
assert(lock.owns_lock());
156167
}
157168

169+
// #76807 Hangs in std::condition_variable_any when used with std::stop_token
170+
{
171+
class MyThread {
172+
public:
173+
MyThread() {
174+
thread_ = support::make_test_jthread([this](std::stop_token st) {
175+
while (!st.stop_requested()) {
176+
std::unique_lock lock{m_};
177+
cv_.wait_for(lock, st, 1h, [] { return false; });
178+
}
179+
});
180+
}
181+
182+
private:
183+
std::mutex m_;
184+
std::condition_variable_any cv_;
185+
std::jthread thread_;
186+
};
187+
188+
ElapsedTimeCheck check(10min);
189+
190+
[[maybe_unused]] MyThread my_thread;
191+
}
192+
193+
// request_stop potentially in-between check and wait
194+
{
195+
std::stop_source ss;
196+
std::condition_variable_any cv;
197+
Mutex mutex;
198+
Lock lock{mutex};
199+
200+
std::atomic_bool pred_started = false;
201+
std::atomic_bool request_stop_called = false;
202+
auto thread = support::make_test_thread([&]() {
203+
pred_started.wait(false);
204+
ss.request_stop();
205+
request_stop_called.store(true);
206+
request_stop_called.notify_all();
207+
});
208+
209+
ElapsedTimeCheck check(10min);
210+
211+
std::same_as<bool> auto r = cv.wait_for(lock, ss.get_token(), 1h, [&]() {
212+
pred_started.store(true);
213+
pred_started.notify_all();
214+
request_stop_called.wait(false);
215+
return false;
216+
});
217+
assert(!r);
218+
thread.join();
219+
220+
assert(lock.owns_lock());
221+
}
222+
158223
#if !defined(TEST_HAS_NO_EXCEPTIONS)
159224
// Throws: Any exception thrown by pred.
160225
{
@@ -164,6 +229,7 @@ void test() {
164229
Lock lock{mutex};
165230

166231
try {
232+
ElapsedTimeCheck check(10min);
167233
cv.wait_for(lock, ss.get_token(), 1h, []() -> bool { throw 5; });
168234
assert(false);
169235
} catch (int i) {

libcxx/test/std/thread/thread.condition/thread.condition.condvarany/wait_token_pred.pass.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,56 @@ void test() {
107107
assert(lock.owns_lock());
108108
}
109109

110+
// #76807 Hangs in std::condition_variable_any when used with std::stop_token
111+
{
112+
class MyThread {
113+
public:
114+
MyThread() {
115+
thread_ = support::make_test_jthread([this](std::stop_token st) {
116+
while (!st.stop_requested()) {
117+
std::unique_lock lock{m_};
118+
cv_.wait(lock, st, [] { return false; });
119+
}
120+
});
121+
}
122+
123+
private:
124+
std::mutex m_;
125+
std::condition_variable_any cv_;
126+
std::jthread thread_;
127+
};
128+
129+
[[maybe_unused]] MyThread my_thread;
130+
}
131+
132+
// request_stop potentially in-between check and wait
133+
{
134+
std::stop_source ss;
135+
std::condition_variable_any cv;
136+
Mutex mutex;
137+
Lock lock{mutex};
138+
139+
std::atomic_bool pred_started = false;
140+
std::atomic_bool request_stop_called = false;
141+
auto thread = support::make_test_thread([&]() {
142+
pred_started.wait(false);
143+
ss.request_stop();
144+
request_stop_called.store(true);
145+
request_stop_called.notify_all();
146+
});
147+
148+
std::same_as<bool> auto r = cv.wait(lock, ss.get_token(), [&]() {
149+
pred_started.store(true);
150+
pred_started.notify_all();
151+
request_stop_called.wait(false);
152+
return false;
153+
});
154+
assert(!r);
155+
thread.join();
156+
157+
assert(lock.owns_lock());
158+
}
159+
110160
#if !defined(TEST_HAS_NO_EXCEPTIONS)
111161
// Throws: Any exception thrown by pred.
112162
{

0 commit comments

Comments
 (0)