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. It is used as a fake 'recursive mutex' for a class that needs
77 * no thread locking because its externally synchronized.
78 *
79 * AssertExternallySynchronizedMutex is recursive (a recursive-mutex) - or really super-recursive - because it allows
80 * lock/shared_lock to be mixed logically (unlike stdc++ shared_mutex).
81 *
82 * \note This means it is LEGAL to call lock () while holding a shared_lock, IFF that shared_lock is for the
83 * same thread. It is implicitly an 'UpgradeLock'
84 *
85 * Externally synchronized means that some external application control guarantees the section of code (or data)
86 * is only accessed by a single thread.
87 *
88 * This can be used to guarantee the same level of thread safety as provided in the std c++ libraries:
89 * Allow multiple readers (shared locks) from multiple threads, but if any thread has
90 * a lock (writer), then no other threads my read or write lock (in any order).
91 *
92 * In debug builds, it enforces this fact through assertions.
93 *
94 * \note This doesn't guarantee catching all races, but it catches many incorrect thread usage cases
95 *
96 * \note ***Not Cancelation Point***
97 *
98 * \note methods all noexcept (just asserts out on problems) - noexcept so debug semantics same as release semantics
99 * Since the DEBUG version will allocate memory, which may fail, those failures trigger assertion failure and abort.
100 *
101 * \note typically used as
102 * qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS Debug::AssertExternallySynchronizedMutex fThisAssertExternallySynchronized_;
103 *
104 * \note Satisfies Concepts:
105 * o movable<AssertExternallySynchronizedMutex>
106 * o copyable<AssertExternallySynchronizedMutex>
107 * o Common::StdCompat::Lockable<AssertExternallySynchronizedMutex>
108 *
109 * \par Example Usage
110 * \code
111 * struct foo {
112 * qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS Debug::AssertExternallySynchronizedMutex fThisAssertExternallySynchronized_;
113 * inline void DoReadWriteStuffOnData ()
114 * {
115 * AssertExternallySynchronizedMutex::WriteContext declareContext { fThisAssertExternallySynchronized_ };
116 * // now do what you usually do for to modify locked data...
117 * }
118 * inline void DoReadOnlyStuffOnData ()
119 * {
120 * AssertExternallySynchronizedMutex::ReadContext declareContext { fThisAssertExternallySynchronized_ };
121 * // now do what you usually do for DoReadOnlyStuffOnData - reading data only...
122 * }
123 * };
124 * \endcode
125 *
126 * \par Example Usage
127 * \code
128 * // this style of use - subclassing - is especially useful if the object foo will be subclassed, and checked throughout the
129 * // code (or subclasses) with Debug::AssertExternallySynchronizedMutex::ReadContext (or WriteContext)
130 * struct foo : public Debug::AssertExternallySynchronizedMutex {
131 * inline void DoReadWriteStuffOnData ()
132 * {
133 * AssertExternallySynchronizedMutex::WriteContext declareContext { *this }; // lock_guard or scopedLock or unique_lock
134 * // now do what you usually do for to modify locked data...
135 * }
136 * inline void DoReadOnlyStuffOnData ()
137 * {
138 * AssertExternallySynchronizedMutex::ReadContext declareContext { *this };
139 * // now do what you usually do for DoReadOnlyStuffOnData - reading data only...
140 * }
141 * };
142 * \endcode
143 *
144 * \note This is SUPER-RECURSIVE lock. It allows lock() when shared_lock held (by only this thread) - so upgrades the lock.
145 * And it allows shared_lock when lock held by the same thread. Otherwise it asserts when a thread conflict is found.
146 * lock() and shared_lock () - here - are NEVER blocking. They just assert there is no conflict.
147 */
149#if qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
150 public:
151 /**
152 * Explicit shared context object, so we can construct multiple AssertExternallySynchronizedMutex which all
153 * share a common 'sharedContext' - representing that they ALL must be externally synchronized across all the cooperating objects
154 *
155 * In most cases, just ignore this class.
156 *
157 * To have N cooperating classes (e.g. object, and a few direct members) all share the same rules of single-threading (treating them all
158 * as one object for the purpose of the rules of safe multithread access) - arrange for them to share a common 'sharedContext'
159 *
160 * \note class marked final to make more clear why safe to not have virtual destructor
161 */
162 struct SharedContext final {
163 public:
164 SharedContext () noexcept = default;
165 SharedContext (const SharedContext&) = delete;
166 SharedContext& operator= (const SharedContext&) = delete;
167 ~SharedContext ();
168
169 private:
170 atomic_uint_fast32_t fFullLocks_{0};
171 thread::id fThreadWithFullLock_; // or value undefined/last value where it had full lock
172
173 private:
174 // Use of inline array avoids mallocs, and makes this run slightly faster. No semantic differerence,
175 // just makes debug mode a bit faster.
176 static constexpr size_t kInlineSharedLockBufSize_ = 2;
177 struct {
178 // most logically a multiset, but std::multiset is not threadsafe and requires external locking.
179 // So does forward_list, but its closer to lock free, so try it for now
180 // GetSharedLockMutexThreads_ () used to access fSharedLocks_
181 array<thread::id, kInlineSharedLockBufSize_> fInitialThreads_;
182 uint8_t fInitialThreadsSize_{0}; // not sure how to add this field only conditionally
183 forward_list<thread::id> fOverflowThreads_;
184 } fSharedLocks_;
185
186 private:
187 bool GetSharedLockEmpty_ () const;
188 pair<size_t, size_t> CountSharedLockThreads_ () const;
189 size_t GetSharedLockThreadsCount_ () const;
190 size_t CountOfIInSharedLockThreads_ (thread::id i) const;
191 void AddSharedLock_ (thread::id i);
192 void RemoveSharedLock_ (thread::id i);
193
194 private:
196 };
197#endif
198
199 public:
200 /**
201 * \note Copy/Move constructor checks for existing locks while copying.
202 * Must be able to read lock source on copy, and have zero existing locks on src for move.
203 * These 'constructors' don't really do/copy/move anything, but just check the state of their own
204 * lock count and the state of the 'src' lock counts.
205 *
206 * NOTE - the 'SharedContext' does NOT get copied by copy constructors, move constructors etc. Its tied
207 * to the l-value.
208 */
209#if qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
210 AssertExternallySynchronizedMutex (const shared_ptr<SharedContext>& sharedContext = nullptr) noexcept;
211 AssertExternallySynchronizedMutex (const shared_ptr<SharedContext>& sharedContext, AssertExternallySynchronizedMutex&& src) noexcept;
214 AssertExternallySynchronizedMutex (const shared_ptr<SharedContext>& sharedContext, const AssertExternallySynchronizedMutex& src) noexcept;
215#else
216 constexpr AssertExternallySynchronizedMutex () noexcept = default;
219#endif
220
221 public:
222 /**
223 * \note operator= checks for existing locks while copying.
224 * Must be able to read lock source on copy, and have zero existing locks on target or move.
225 */
228
229#if qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
230 public:
231 nonvirtual shared_ptr<SharedContext> GetSharedContext () const;
232
233 public:
234 /**
235 * Make it easy for subclasses to expose SetAssertExternallySynchronizedMutexContext () functionality, so those
236 * subclasses can allow users of those classes to share a sharing context.
237 *
238 * \note - this is named without the prefixing '_' (though protected) to make it easier to forward, just using using.
239 */
240 nonvirtual void SetAssertExternallySynchronizedMutexContext (const shared_ptr<SharedContext>& sharedContext);
241#endif
242
243 public:
244 /**
245 * Saves current thread, and increments lock count, and
246 * \pre already locked by this thread or no existing locks (either shared or exclusive)
247 *
248 * \note method non-const (can always const_cast if needed) because of standard C++ convention of non-const objects
249 * for write-lock
250 */
251 nonvirtual void lock () noexcept;
252
253 public:
254 /**
255 * \brief Like lock() - if it would succeed, same then, but if would fail instead of assert out, just return false.
256 */
257 nonvirtual bool try_lock () noexcept;
258
259 public:
260 /**
261 * Just decrement lock count
262 *
263 * \pre still running on the same locking thread and locks not unbalanced
264 */
265 nonvirtual void unlock () noexcept;
266
267 public:
268 /**
269 * Saves current thread (multiset), and increments shared count, and
270 * \pre no pre-existing locks on other threads
271 *
272 * \note method const despite usual lockable rules, so easier to work with 'const' objects being 'marked' as doing a read operation.
273 */
274 nonvirtual void lock_shared () const noexcept;
275
276 public:
277 /**
278 * Just decrement shared lock count (remove this thread from shared lock multiset)
279 *
280 * \note see lock_shard for why const.
281 *
282 * \pre still running on the same locking thread and locks not unbalanced
283 */
284 nonvirtual void unlock_shared () const noexcept;
285
286 public:
287 /**
288 * \brief Instantiate AssertExternallySynchronizedMutex::ReadContext to designate an area of code where protected data will be read
289 *
290 * This type alias makes a little more clear in reading code that the 'lock' is really just an assertion about thread safety
291 *
292 * Since AssertExternallySynchronizedMutex follows the concept 'mutex' you can obviously use any
293 * of the standard lockers in std::c++, but using AssertExternallySynchronizedMutex::ReadContext - makes it a little more clear
294 * self-documenting in your code, that you are doing this in a context where you are only reading the pseudo-locked data.
295 *
296 * \note we get away with 'const' in shared_lock<const AssertExternallySynchronizedMutex> because we chose to make
297 * lock_shared, and unlock_shared const methods (see their docs above).
298 *
299 * \note - though CTOR not declared noexcept, ReadContext cannot throw an exception (it asserts out on failure)
300 */
302 static_assert (movable<ReadContext> and not copyable<ReadContext>);
303
304 public:
305 /**
306 * \brief Instantiate AssertExternallySynchronizedMutex::WriteContext to designate an area of code where protected data will be written
307 *
308 * This type alias makes a little more clear in reading code that the 'lock' is really just an assertion about thread safety
309 *
310 * Since AssertExternallySynchronizedMutex follows the concept 'mutex' you can obviously use any
311 * of the standard lockers in std::c++, but using AssertExternallySynchronizedMutex::WriteContext - makes it a little more clear
312 * self-documenting in your code, that you are doing this in a context where you are only writing the pseudo-locked data.
313 *
314 * Plus, the fact that it forces a non-const interpretation on the object in question (by using lock_guard of a non-const AssertExternallySynchronizedMutex)
315 * makes it a little easier to catch cases where you accidentally use WriteContext and meant ReadContext.
316 *
317 * \note - used lock_guard before Stroika v3.0d10, but switched to unique_lock so movable (handy in some cases).
318 * And performance not much of an issue since this is all debug-only code.
319 *
320 * \note - though CTOR not declared noexcept, WriteContext cannot throw an exception (it asserts out on failure)
321 */
323 static_assert (movable<WriteContext> and not copyable<WriteContext>);
324
325#if qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
326 private:
327 nonvirtual void lock_ () noexcept;
328 nonvirtual bool try_lock_ () noexcept;
329 nonvirtual void unlock_ () noexcept;
330 nonvirtual void lock_shared_ () const noexcept;
331 nonvirtual void unlock_shared_ () const noexcept;
332
333 private:
334 shared_ptr<SharedContext> fSharedContext_;
335
336 private:
337 static mutex& GetSharedLockMutexThreads_ (); // MUTEX ONLY FOR fSharedLocks_ (could do one mutex per AssertExternallySynchronizedMutex but static probably performs better)
338#endif
339 };
340 // see Satisfies Concepts:
341 static_assert (movable<AssertExternallySynchronizedMutex> and copyable<AssertExternallySynchronizedMutex> and
343
344}
345
346/*
347 ********************************************************************************
348 ***************************** Implementation Details ***************************
349 ********************************************************************************
350 */
351#include "AssertExternallySynchronizedMutex.inl"
352
353#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