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