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