Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
ConditionVariable.h
Go to the documentation of this file.
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#ifndef _Stroika_Foundation_Execution_ConditionVariable_h_
5#define _Stroika_Foundation_Execution_ConditionVariable_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include <condition_variable>
10#include <mutex>
11
13
14/**
15 * \note Code-Status: <a href="Code-Status.md#Beta">Beta</a>
16 *
17 * \file
18 *
19 */
20
22
23 /**
24 * NOTE - XCode 15 still has this limitation
25 * #if __cpp_lib_jthread < 201911
26 * #warning "NOTE __cpp_lib_jthread < 201911: see if we can lose tests"
27 * #endif
28 * -- LGP 2024-09-17
29 */
30
31 /**
32 * ConditionVariable is a thin abstraction/wrapper on a std::condition_variable, with support for Stroika thread Cancelation, and
33 * other shortcuts to simplify use. (combines related mutex with condition variable). Since you always use a
34 * condition variable with a mutex, its helpful to also include the associated mutex in the condition variable (type can be
35 * changed with template parameters).
36 *
37 * Intentionally leave 'mutex' and 'condition_variable' elements public, because users will OFTEN
38 * need to directly access a reference to the mutex, and so there is little opportunity to hide.
39 *
40 * This is just meant to codify some good practices and share some code. Its a VERY thin wrapper - if even
41 * that - on the std::mutex.
42 *
43 * \par Example Usage
44 * \code
45 * ConditionVariable<> fConditionVariable;
46 * bool fTriggered = false;
47 *
48 * // THREAD A:
49 * fConditionVariable.MutateDataNotifyAll ([=]() { fTriggered = true; });
50 *
51 * // THREAD B:
52 * std::unique_lock<mutex> lock (fConditionVariable.fMutex);
53 * if (fConditionVariable.wait_until (lock, timeoutAt, [this]() { return fTriggered; })) {
54 * REACT TO CHANGE ();
55 * }
56 * else {
57 * Throw (TimeOutException::kThe);
58 * }
59 * \endcode
60 *
61 * \note Design Note on implementing thread cancelation
62 * (@todo UPDATE DOCS IN LIGHT OF C++20)
63 * > There are three obvious ways to implement cancelation
64 *
65 * > Using EINTR (POSIX) and alertable states on windows - this is what we do most other places
66 * in Stroika for thread cancelability.
67 *
68 * Sadly, the POSIX (gcc/clang) of condition_variable don't appear to support EINTR, the windows
69 * implementation doesn't support alertable states, and (FIND REFERENCE***) I THINK I saw documented
70 * someplace the standard mentions that this cannot be done (no idea why)
71 *
72 * > Maintain a list of 'waiting' condition variables, and then have thread-abort notify_all() on each of these
73 * This significantly adds to the cost of waiting (add/remove safely from linked list with thread safety) - but
74 * that's probably still the best approach.
75 *
76 * Also have to code this very carefully to avoid deadlocks, since the cancelation code needs to lock something to
77 * allow it to safely signal the condition variable.
78 *
79 * ***Probably best approach but trickier todo***
80 *
81 * > Just wake periodically (shorten the wait time) and look for aborts). This is arguably the most costly approach
82 * but also simplest to implement, and what we have for now (as of January 2018, version v2.0a225)
83 *
84 * \note From http://en.cppreference.com/w/cpp/thread/condition_variable
85 * "Even if the shared variable is atomic, it must be modified under the mutex in order
86 * to correctly publish the modification to the waiting thread."
87 *
88 * I believe this is because:
89 * The total ordering created by this pattern of update is crucial!
90 *
91 * Thread A lock and then unlocks mutex, so change of state of triggered always BEFORE (or after)
92 * the change of state in the condition variable mutex, so no ambiguity about when fTriggered set.
93 *
94 * If its set before, condition variable sees that on entry (in its pred check).
95 *
96 * If its after, then on the mutex unlock (I DON'T KNOW HOW THIS HAPPENS YET) - the Condition
97 * var must somehow get 'awakened - probably by a TLS list (queue) of waiters to get notified
98 * and that gets hooked in the mutex code???? GUESSING.
99 *
100 * So anyhow - note - its critical when changing values of underlying 'condition' - to wrap that in mutex
101 * lock/unlock.
102 *
103 * This MAYBE related to why "std::condition_variable works only with std::unique_lock<std::mutex>".
104 * BUT - then i don't understand how " std::condition_variable_any provides a condition variable that works with any BasicLockable"
105 *
106 * \note use of condition_variable (not condition_variable_any) with stop_token
107 * See https://stackoverflow.com/questions/66309276/why-does-c20-stdcondition-variable-not-support-stdstop-token
108 * Before Stroika v3, we used
109 * typename CONDITION_VARIABLE = conditional_t<same_as<mutex, MUTEX>, condition_variable, condition_variable_any>
110 * For better stop_token interop, we've changed the default to
111 * typename CONDITION_VARIABLE = condition_variable_any
112 */
113 template <typename MUTEX = mutex, typename CONDITION_VARIABLE = condition_variable_any>
115 /**
116 * This is the type of the mutex associated with the condition variable.
117 */
118 using MutexType = MUTEX;
119
120 /**
121 * This is the type of condition variable. This is generally going to be condition_variable_any (so it will work with stop_tokens)
122 * but could be condition_variable (or others).
123 */
124 using ConditionVariableType = CONDITION_VARIABLE;
125
126 /**
127 * explicitly unlockable lock (on the mutex). Use, for example, with condition variables, where the apis need to unlock/lock and track
128 * the 'locked' state.
129 */
130 using LockType = unique_lock<MUTEX>;
131
132 /**
133 * just lock and unlock. Basically the same as LockType, but less flexible (cannot explicitly unlock) and more efficient (cuz no data
134 * to track if locked).
135 */
136 using QuickLockType = lock_guard<MUTEX>;
137
138 /*
139 * https://stackoverflow.com/questions/66309276/why-does-c20-stdcondition-variable-not-support-stdstop-token
140 *
141 * Seems simple enuf to use
142 * std::stop_callback callback{ stoken, [&cv]{ cv.notify_all(); } };
143 * But according to that stackoverflow link, its unsafe. MSFT implementation basically does this anyhow.
144 * So only support stop_token for condition_variable_any.
145 * -- LGP 2023-10-06
146 */
147#if __cpp_lib_jthread < 201911
148 static constexpr bool kSupportsStopToken = false;
149#else
150 static constexpr bool kSupportsStopToken = same_as<CONDITION_VARIABLE, condition_variable_any>;
151#endif
152
153 /**
154 * sConditionVariableWaitChunkTime is used iff kSupportsStopToken is false.
155 *
156 * When waiting, with kSupportsStopToken false, this max timeout is used chunk the waits into smaller chunks so
157 * we can check for thread cancelation.
158 *
159 * This time value is generally NOT used in condition variable operation, except in a few disparate situations:
160 *
161 * o stop_token overloads not supported (see kSupportsStopToken)
162 * o ? may change - cannot interrupt main thread (abort) so have no stop token to use/pass? why does that matter?
163 *
164 * \note in Stroika v2.1 this was called sThreadAbortCheckFrequency_Default
165 */
167
168 /**
169 */
170 ConditionVariable () = default;
171 ConditionVariable (const ConditionVariable&) = delete;
172 nonvirtual ConditionVariable& operator= (const ConditionVariable&) = delete;
173
174 /**
175 * ConditionVariable is a very THIN abstraction. Callers will often need to explicitly access/use the mutex
176 */
178
179 /**
180 * ConditionVariable is a very THIN abstraction. Callers will often need to explicitly access/use the condition_variable
181 */
182 CONDITION_VARIABLE fConditionVariable;
183
184 /**
185 * NOTIFY the condition variable (notify_one), but unlock first due to:
186 * http://en.cppreference.com/w/cpp/thread/condition_variable/notify_all
187 *
188 * The notifying thread does not need to hold the lock on the same mutex as the
189 * one held by the waiting thread(s); in fact doing so is a pessimization, since
190 * the notified thread would immediately block again, waiting for the notifying
191 * thread to release the lock.
192 *
193 * \note ***NOT a Cancelation Point***
194 */
195 nonvirtual void release_and_notify_one (LockType& lock) noexcept;
196
197 /**
198 * NOTIFY the condition variable (notify_all), but unlock first due to:
199 * http://en.cppreference.com/w/cpp/thread/condition_variable/notify_all
200 *
201 * The notifying thread does not need to hold the lock on the same mutex as the
202 * one held by the waiting thread(s); in fact doing so is a pessimization, since
203 * the notified thread would immediately block again, waiting for the notifying
204 *
205 * \note ***NOT a Cancelation Point***
206 */
207 nonvirtual void release_and_notify_all (LockType& lock) noexcept;
208
209 /**
210 * \brief forward notify_one () call to underlying std::condition_variable'
211 *
212 * \note ***NOT a Cancelation Point***
213 */
214 nonvirtual void notify_one () noexcept;
215
216 /**
217 * \brief forward notify_all () call to underlying std::condition_variable'
218 *
219 * \note ***NOT a Cancelation Point***
220 */
221 nonvirtual void notify_all () noexcept;
222
223 /**
224 * Like condition_variable wait_until, except
225 * using float instead of chrono (fix)
226 * supports Stroika thread interruption
227 *
228 * readyToWake - predicate which returns false if the waiting should be continued.
229 *
230 * Returns:
231 * 1) std::cv_status::timeout if the relative timeout specified by rel_time expired, std::cv_status::no_timeout otherwise.
232 * 2) true of 'readyToWake' () is reason we woke
233 *
234 * \note ***Cancelation Point***
235 *
236 * \note The intention here is to be semantically IDENTICAL to condition_variable::wait_until () - except
237 * for adding support for thread interruption (and a minor point - Time::TimePointSeconds)
238 *
239 * \note the cv_status returning overload CAN return/wakeup spuriously (not timeout, and not desired condition true)
240 * The PREDICATE (readyToWake) overload, never returns a spurious wake (so only returns timeout (false) or true if readyToWake returned true.
241 *
242 * \pre (lock.owns_lock ());
243 * \post (lock.owns_lock ());
244 *
245 * @todo maybe lose the (no predicate) overload --LGP 2023-10-09
246 */
247 nonvirtual cv_status wait_until (LockType& lock, Time::TimePointSeconds timeoutAt);
248 template <invocable PREDICATE>
249 nonvirtual bool wait_until (LockType& lock, Time::TimePointSeconds timeoutAt, PREDICATE&& readyToWake);
250
251 /**
252 * Like condition_variable wait_for, except
253 * using float instead of chrono (fix)
254 * supports Stroika thread interruption
255 *
256 * readyToWake - predicate which returns false if the waiting should be continued.
257 *
258 * Returns:
259 * 1) std::cv_status::timeout if the relative timeout specified by rel_time expired, std::cv_status::no_timeout otherwise.
260 * 2) true of 'readyToWake' () is reason we woke
261 *
262 * \note ***Cancelation Point***
263 *
264 * \note The intention here is to be semantically IDENTICAL to condition_variable::wait_for () - except
265 * for adding support for thread interruption (and a minor point - Time::TimePointSeconds)
266 *
267 * \note the cv_status returning overload CAN return/wakeup spuriously (not timeout, and not desired condition true)
268 * The PREDICATE (readyToWake) overload, never returns a spurious wake (so only returns timeout (false) or true if readyToWake returned true.
269 *
270 * \pre (lock.owns_lock ());
271 * \post (lock.owns_lock ());
272 */
273 nonvirtual cv_status wait_for (LockType& lock, Time::DurationSeconds timeout);
274 template <invocable PREDICATE>
275 nonvirtual bool wait_for (LockType& lock, Time::DurationSeconds timeout, PREDICATE&& readyToWake);
276
277 /**
278 * Idea is you pass lambda to do actual data change, and this acquires lock first, and notifies all after.
279 *
280 * \par Example Usage
281 * \code
282 * // from BlockingQueue code...
283 * fConditionVariable_.MutateDataNotifyAll ([=]() { fEndOfInput_ = true; });
284 * \endcode
285 *
286 * \note ***Not Cancelation Point*** -- but perhaps should be???
287 */
288 template <invocable FUNCTION>
289 nonvirtual void MutateDataNotifyAll (FUNCTION&& mutatorFunction);
290
291 /**
292 * Idea is you pass lambda to do actual data change, and this acquires lock first, and notifies one after.
293 *
294 * \par Example Usage
295 * \code
296 * // from BlockingQueue code...
297 * fConditionVariable_.MutateDataNotifyOne ([=]() { fEndOfInput_ = true; });
298 * \endcode
299 *
300 * \note ***Not Cancelation Point*** -- but perhaps should be???
301 */
302 template <invocable FUNCTION>
303 nonvirtual void MutateDataNotifyOne (FUNCTION&& mutatorFunction);
304 };
305
306}
307
308/*
309 ********************************************************************************
310 ***************************** Implementation Details ***************************
311 ********************************************************************************
312 */
313#include "ConditionVariable.inl"
314
315#endif /*_Stroika_Foundation_Execution_ConditionVariable_h_*/
chrono::duration< double > DurationSeconds
chrono::duration<double> - a time span (length of time) measured in seconds, but high precision.
Definition Realtime.h:57
nonvirtual void notify_all() noexcept
forward notify_all () call to underlying std::condition_variable'
nonvirtual void release_and_notify_one(LockType &lock) noexcept
nonvirtual cv_status wait_until(LockType &lock, Time::TimePointSeconds timeoutAt)
nonvirtual void MutateDataNotifyAll(FUNCTION &&mutatorFunction)
nonvirtual cv_status wait_for(LockType &lock, Time::DurationSeconds timeout)
static Time::DurationSeconds sConditionVariableWaitChunkTime
nonvirtual void notify_one() noexcept
forward notify_one () call to underlying std::condition_variable'
nonvirtual void release_and_notify_all(LockType &lock) noexcept
nonvirtual void MutateDataNotifyOne(FUNCTION &&mutatorFunction)