Stroika Library 3.0d23
 
Loading...
Searching...
No Matches
AssertExternallySynchronizedMutex.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_Debug_AssertExternallySynchronizedMutex_h_
5#define _Stroika_Foundation_Debug_AssertExternallySynchronizedMutex_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include <algorithm>
10#include <array>
11#include <atomic>
12#include <forward_list>
13#include <memory>
14#include <mutex>
15#include <optional>
16#include <shared_mutex>
17#include <thread>
18
19#include "Stroika/Foundation/Common/Common.h"
20#include "Stroika/Foundation/Common/Concepts.h"
24
25/**
26 * \file
27 *
28 * \note Code-Status: <a href="Code-Status.md#Release">Release</a>
29 *
30 * TODO:
31 * @todo see if fSharedLocks_ can be replaced with LOCK-FREE - at least 99% of the time.... Locks affect timing, and can hide thread
32 * bugs. Quickie attempt at profiling yields that that time is NOT spent with the locks but with the remove()
33 * code (since I switched from multiset to forward_list, so maybe cuz of that). Or could be bad measurement (I just
34 * test on DEBUG builds).
35 *
36 * Since Stroika 2.1b10 we do have a lock/free forward_list class I could try. But I'm not yet confident
37 * in its stability, so maybe sometime down the road...
38 *
39 * @see http://stroika-bugs.sophists.com/browse/STK-540 for details on stuff todo above
40 *
41 * @todo Reconsider if AssertExternallySynchronizedMutex::operator= should allow for this to be locked
42 * by the current thread. Safe to do later as that would be weakening the current check/requirement.
43 */
44
45namespace Stroika::Foundation::Debug {
46
47 /**
48 * \brief qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled controls if this threaded access protection
49 *
50 * The compilation compile-time macro qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled can be used
51 * to control if AssertExternallySynchronizedMutex checking is enabled.
52 *
53 * If its not defined (typical), we look at qStroika_Foundation_Debug_AssertionsChecked. If that is false, qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled is disabled.
54 *
55 * If qStroika_Foundation_Debug_AssertionsChecked is true, BUT, we have TSAN enabled, we STILL (change in Stroika v3.0d1) - DISABLE kAssertExternallySynchronizedMutexEnabled
56 * since its slow, and redundant.
57 *
58 * Only if qStroika_Foundation_Debug_AssertionsChecked is true, there is no TSAN, and qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled is made
59 * do we turn on qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled.
60 *
61 * \note TRIED to do this with constexpr bool kAssertExternallySynchronizedMutexEnabled, but as of C++20 rules
62 * still too much of a PITA to use: cannot conditionally define classes, and nearly anything
63 * based on requires/if constexpr, unless it is a template.
64 */
65#if not defined(qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled)
66#if qStroika_Foundation_Debug_AssertionsChecked and not Stroika_Foundation_Debug_Sanitizer_HAS_ThreadSanitizer
67#define qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled 1
68#else
69#define qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled 0
70#endif
71#endif
72
73 /**
74 * \brief NOT a real mutex - just a debugging infrastructure support tool so in debug builds can be assured threadsafe, which is syntactically used like a mutex, for SIMILAR reasons in similar places
75 *
76 * This class is a 'no op' in production builds (so zero cost in release builds, assumes caller ensures thread safety).
77 *
78 * AssertExternallySynchronizedMutex follows the pattern of (a recursive-mutex) - or really super-recursive - because it allows
79 * lock/shared_lock to be mixed logically (unlike stdc++ shared_mutex).
80 *
81 * \note This means it is LEGAL to call lock () while holding a shared_lock, IFF that shared_lock is for the
82 * same thread. It is implicitly an 'UpgradeLock'
83 *
84 * Externally synchronized means that some external application control guarantees the section of code (or data)
85 * is only accessed by a single thread.
86 *
87 * This can be used to guarantee the same level of thread safety as provided in the std c++ libraries:
88 * Allow multiple readers (shared locks) from multiple threads, but if any thread has
89 * a lock (writer), then no other threads my read or write lock (in any order).
90 *
91 * In debug builds, it enforces this fact through assertions.
92 *
93 * \note This doesn't guarantee catching all races, but it catches many incorrect thread usage cases
94 *
95 * \note ***Not Cancelation Point***
96 *
97 * \note methods all noexcept (just asserts out on problems) - noexcept so debug semantics same as release semantics
98 * Since the DEBUG version will allocate memory, which may fail, those failures trigger assertion failure and abort.
99 *
100 * \note typically used as
101 * qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS Debug::AssertExternallySynchronizedMutex fThisAssertExternallySynchronized_;
102 *
103 * \note Satisfies Concepts:
104 * o movable<AssertExternallySynchronizedMutex>
105 * o copyable<AssertExternallySynchronizedMutex>
106 * o Common::StdCompat::Lockable<AssertExternallySynchronizedMutex>
107 *
108 * \par Example Usage
109 * \code
110 * struct foo {
111 * qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS Debug::AssertExternallySynchronizedMutex fThisAssertExternallySynchronized_;
112 * inline void DoReadWriteStuffOnData ()
113 * {
114 * AssertExternallySynchronizedMutex::WriteContext declareContext { fThisAssertExternallySynchronized_ };
115 * // now do what you usually do for to modify locked data...
116 * }
117 * inline void DoReadOnlyStuffOnData ()
118 * {
119 * AssertExternallySynchronizedMutex::ReadContext declareContext { fThisAssertExternallySynchronized_ };
120 * // now do what you usually do for DoReadOnlyStuffOnData - reading data only...
121 * }
122 * };
123 * \endcode
124 *
125 * \par Example Usage
126 * \code
127 * // this style of use - subclassing - is especially useful if the object foo will be subclassed, and checked throughout the
128 * // code (or subclasses) with Debug::AssertExternallySynchronizedMutex::ReadContext (or WriteContext)
129 * struct foo : public Debug::AssertExternallySynchronizedMutex {
130 * inline void DoReadWriteStuffOnData ()
131 * {
132 * AssertExternallySynchronizedMutex::WriteContext declareContext { *this }; // lock_guard or scopedLock or unique_lock
133 * // now do what you usually do for to modify locked data...
134 * }
135 * inline void DoReadOnlyStuffOnData ()
136 * {
137 * AssertExternallySynchronizedMutex::ReadContext declareContext { *this };
138 * // now do what you usually do for DoReadOnlyStuffOnData - reading data only...
139 * }
140 * };
141 * \endcode
142 *
143 * \note This is SUPER-RECURSIVE lock. It allows lock() when shared_lock held (by only this thread) - so upgrades the lock.
144 * And it allows shared_lock when lock held by the same thread. Otherwise it asserts when a thread conflict is found.
145 * lock() and shared_lock () - here - are NEVER blocking. They just assert there is no conflict.
146 */
148#if qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
149 public:
150 /**
151 * Explicit shared context object, so we can construct multiple AssertExternallySynchronizedMutex which all
152 * share a common 'sharedContext' - representing that they ALL must be externally synchronized across all the cooperating objects
153 *
154 * In most cases, just ignore this class.
155 *
156 * To have N cooperating classes (e.g. object, and a few direct members) all share the same rules of single-threading (treating them all
157 * as one object for the purpose of the rules of safe multithread access) - arrange for them to share a common 'sharedContext'
158 *
159 * \note class marked final to make more clear why safe to not have virtual destructor
160 */
161 struct SharedContext final {
162 public:
163 SharedContext () noexcept = default;
164 SharedContext (const SharedContext&) = delete;
165 SharedContext& operator= (const SharedContext&) = delete;
166 ~SharedContext ();
167
168 private:
169 atomic_uint_fast32_t fFullLocks_{0};
170 thread::id fThreadWithFullLock_; // or value undefined/last value where it had full lock
171
172 private:
173 // Use of inline array avoids mallocs, and makes this run slightly faster. No semantic differerence,
174 // just makes debug mode a bit faster.
175 static constexpr size_t kInlineSharedLockBufSize_ = 2;
176 struct {
177 // most logically a multiset, but std::multiset is not threadsafe and requires external locking.
178 // So does forward_list, but its closer to lock free, so try it for now
179 // GetSharedLockMutexThreads_ () used to access fSharedLocks_
180 array<thread::id, kInlineSharedLockBufSize_> fInitialThreads_;
181 uint8_t fInitialThreadsSize_{0}; // not sure how to add this field only conditionally
182 forward_list<thread::id> fOverflowThreads_;
183 } fSharedLocks_;
184
185 private:
186 bool GetSharedLockEmpty_ () const;
187 pair<size_t, size_t> CountSharedLockThreads_ () const;
188 size_t GetSharedLockThreadsCount_ () const;
189 size_t CountOfIInSharedLockThreads_ (thread::id i) const;
190 void AddSharedLock_ (thread::id i);
191 void RemoveSharedLock_ (thread::id i);
192
193 private:
195 };
196#endif
197
198 public:
199 /**
200 * \note Copy/Move constructor checks for existing locks while copying.
201 * Must be able to read lock source on copy, and have zero existing locks on src for move.
202 * These 'constructors' don't really do/copy/move anything, but just check the state of their own
203 * lock count and the state of the 'src' lock counts.
204 *
205 * NOTE - the 'SharedContext' does NOT get copied by copy constructors, move constructors etc. Its tied
206 * to the l-value.
207 */
208#if qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
209 AssertExternallySynchronizedMutex (const shared_ptr<SharedContext>& sharedContext = nullptr) noexcept;
210 AssertExternallySynchronizedMutex (const shared_ptr<SharedContext>& sharedContext, AssertExternallySynchronizedMutex&& src) noexcept;
213 AssertExternallySynchronizedMutex (const shared_ptr<SharedContext>& sharedContext, const AssertExternallySynchronizedMutex& src) noexcept;
214#else
215 constexpr AssertExternallySynchronizedMutex () noexcept = default;
218#endif
219
220 public:
221 /**
222 * \note operator= checks for existing locks while copying.
223 * Must be able to read lock source on copy, and have zero existing locks on target or move.
224 */
227
228#if qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
229 public:
230 nonvirtual shared_ptr<SharedContext> GetSharedContext () const;
231
232 public:
233 /**
234 * Make it easy for subclasses to expose SetAssertExternallySynchronizedMutexContext () functionality, so those
235 * subclasses can allow users of those classes to share a sharing context.
236 *
237 * \note - this is named without the prefixing '_' (though protected) to make it easier to forward, just using using.
238 */
239 nonvirtual void SetAssertExternallySynchronizedMutexContext (const shared_ptr<SharedContext>& sharedContext);
240#endif
241
242 public:
243 /**
244 * Saves current thread, and increments lock count, and
245 * \pre already locked by this thread or no existing locks (either shared or exclusive)
246 *
247 * \note method non-const (can always const_cast if needed) because of standard C++ convention of non-const objects
248 * for write-lock
249 */
250 nonvirtual void lock () noexcept;
251
252 public:
253 /**
254 * \brief Like lock() - if it would succeed, same then, but if would fail instead of assert out, just return false.
255 */
256 nonvirtual bool try_lock () noexcept;
257
258 public:
259 /**
260 * Just decrement lock count
261 *
262 * \pre still running on the same locking thread and locks not unbalanced
263 */
264 nonvirtual void unlock () noexcept;
265
266 public:
267 /**
268 * Saves current thread (multiset), and increments shared count, and
269 * \pre no pre-existing locks on other threads
270 *
271 * \note method const despite usual lockable rules, so easier to work with 'const' objects being 'marked' as doing a read operation.
272 */
273 nonvirtual void lock_shared () const noexcept;
274
275 public:
276 /**
277 * Just decrement shared lock count (remove this thread from shared lock multiset)
278 *
279 * \note see lock_shard for why const.
280 *
281 * \pre still running on the same locking thread and locks not unbalanced
282 */
283 nonvirtual void unlock_shared () const noexcept;
284
285 public:
286 /**
287 * \brief Instantiate AssertExternallySynchronizedMutex::ReadContext to designate an area of code where protected data will be read
288 *
289 * This type alias makes a little more clear in reading code that the 'lock' is really just an assertion about thread safety
290 *
291 * Since AssertExternallySynchronizedMutex follows the concept 'mutex' you can obviously use any
292 * of the standard lockers in std::c++, but using AssertExternallySynchronizedMutex::ReadContext - makes it a little more clear
293 * self-documenting in your code, that you are doing this in a context where you are only reading the pseudo-locked data.
294 *
295 * \note we get away with 'const' in shared_lock<const AssertExternallySynchronizedMutex> because we chose to make
296 * lock_shared, and unlock_shared const methods (see their docs above).
297 *
298 * \note - though CTOR not declared noexcept, ReadContext cannot throw an exception (it asserts out on failure)
299 */
301 static_assert (movable<ReadContext> and not copyable<ReadContext>);
302
303 public:
304 /**
305 * \brief Instantiate AssertExternallySynchronizedMutex::WriteContext to designate an area of code where protected data will be written
306 *
307 * This type alias makes a little more clear in reading code that the 'lock' is really just an assertion about thread safety
308 *
309 * Since AssertExternallySynchronizedMutex follows the concept 'mutex' you can obviously use any
310 * of the standard lockers in std::c++, but using AssertExternallySynchronizedMutex::WriteContext - makes it a little more clear
311 * self-documenting in your code, that you are doing this in a context where you are only writing the pseudo-locked data.
312 *
313 * Plus, the fact that it forces a non-const interpretation on the object in question (by using lock_guard of a non-const AssertExternallySynchronizedMutex)
314 * makes it a little easier to catch cases where you accidentally use WriteContext and meant ReadContext.
315 *
316 * \note - used lock_guard before Stroika v3.0d10, but switched to unique_lock so movable (handy in some cases).
317 * And performance not much of an issue since this is all debug-only code.
318 *
319 * \note - though CTOR not declared noexcept, WriteContext cannot throw an exception (it asserts out on failure)
320 */
322 static_assert (movable<WriteContext> and not copyable<WriteContext>);
323
324#if qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
325 private:
326 nonvirtual void lock_ () noexcept;
327 nonvirtual bool try_lock_ () noexcept;
328 nonvirtual void unlock_ () noexcept;
329 nonvirtual void lock_shared_ () const noexcept;
330 nonvirtual void unlock_shared_ () const noexcept;
331
332 private:
333 shared_ptr<SharedContext> fSharedContext_;
334
335 private:
336 static mutex& GetSharedLockMutexThreads_ (); // MUTEX ONLY FOR fSharedLocks_ (could do one mutex per AssertExternallySynchronizedMutex but static probably performs better)
337#endif
338 };
339 // see Satisfies Concepts:
340 static_assert (movable<AssertExternallySynchronizedMutex> and copyable<AssertExternallySynchronizedMutex> and
342
343}
344
345/*
346 ********************************************************************************
347 ***************************** Implementation Details ***************************
348 ********************************************************************************
349 */
350#include "AssertExternallySynchronizedMutex.inl"
351
352#endif /*_Stroika_Foundation_Debug_AssertExternallySynchronizedMutex_h_*/
NOT a real mutex - just a debugging infrastructure support tool so in debug builds can be assured thr...
nonvirtual AssertExternallySynchronizedMutex & operator=(AssertExternallySynchronizedMutex &&rhs) noexcept
shared_lock< const AssertExternallySynchronizedMutex > ReadContext
Instantiate AssertExternallySynchronizedMutex::ReadContext to designate an area of code where protect...
unique_lock< AssertExternallySynchronizedMutex > WriteContext
Instantiate AssertExternallySynchronizedMutex::WriteContext to designate an area of code where protec...
nonvirtual bool try_lock() noexcept
Like lock() - if it would succeed, same then, but if would fail instead of assert out,...
Logically the C++ standard Lockable named requirement, but that was not included in std c++ library.
Definition StdCompat.h:77