Stroika Library 3.0d24
 
Loading...
Searching...
No Matches
AssertExternallySynchronizedChecker.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_AssertExternallySynchronizedChecker_h_
5#define _Stroika_Foundation_Debug_AssertExternallySynchronizedChecker_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 https://github.com/SophistSolutions/Stroika/issues/676 (STK-540) for details on stuff todo above
40 *
41 * @todo Reconsider if AssertExternallySynchronizedChecker::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_AssertExternallySynchronizedChecker_Enabled controls if this threaded access protection
49 *
50 * The compilation compile-time macro qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled can be used
51 * to control if AssertExternallySynchronizedChecker checking is enabled.
52 *
53 * If its not defined (typical), we look at qStroika_Foundation_Debug_AssertionsChecked. If that is false, qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_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 kAssertExternallySynchronizedCheckerEnabled
56 * since its slow, and redundant.
57 *
58 * Only if qStroika_Foundation_Debug_AssertionsChecked is true, there is no TSAN, and qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled is made
59 * do we turn on qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled.
60 *
61 * \note TRIED to do this with constexpr bool kAssertExternallySynchronizedCheckerEnabled, 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// DEPRECATED spelling (before Stroika v3.0d24 this class was called AssertExternallySynchronizedMutex).
66// Honored so that an existing build defining the old macro keeps the setting it asked for, rather than
67// silently falling through to the default below. Remove when the deprecated header goes.
68#if defined(qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled) and \
69 not defined(qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled)
70#define qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled \
71 qStroika_Foundation_Debug_AssertExternallySynchronizedMutex_Enabled
72#endif
73
74#if not defined(qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled)
75#if qStroika_Foundation_Debug_AssertionsChecked and not Stroika_Foundation_Debug_Sanitizer_HAS_ThreadSanitizer
76#define qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled 1
77#else
78#define qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled 0
79#endif
80#endif
81
82 /**
83 * \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
84 *
85 * This class is a 'no op' in production builds (so zero cost in release builds, assumes caller ensures thread safety).
86 *
87 * AssertExternallySynchronizedChecker follows the pattern of (a recursive-mutex) - or really super-recursive - because it allows
88 * lock/shared_lock to be mixed logically (unlike stdc++ shared_mutex).
89 *
90 * \note This means it is LEGAL to call lock () while holding a shared_lock, IFF that shared_lock is for the
91 * same thread. It is implicitly an 'UpgradeLock'
92 *
93 * Externally synchronized means that some external application control guarantees the section of code (or data)
94 * is only accessed by a single thread.
95 *
96 * This can be used to guarantee the same level of thread safety as provided in the std c++ libraries:
97 * Allow multiple readers (shared locks) from multiple threads, but if any thread has
98 * a lock (writer), then no other threads my read or write lock (in any order).
99 *
100 * In debug builds, it enforces this fact through assertions.
101 *
102 * \note This doesn't guarantee catching all races (with no happens-before), but it catches many incorrect thread usage cases
103 *
104 * \note ***Not Cancelation Point***
105 *
106 * \note methods all noexcept (just asserts out on problems) - noexcept so debug semantics same as release semantics
107 * Since the DEBUG version will allocate memory, which may fail, those failures trigger assertion failure and abort.
108 *
109 * \note typically used as
110 * qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS Debug::AssertExternallySynchronizedChecker fThisAssertExternallySynchronized_;
111 *
112 * \note Satisfies Concepts:
113 * o movable<AssertExternallySynchronizedChecker>
114 * o copyable<AssertExternallySynchronizedChecker>
115 * o Common::StdCompat::Lockable<AssertExternallySynchronizedChecker>
116 *
117 * \note movable/copyable is REQUIRED, and is where this most visibly parts company with a real mutex
118 * (std::mutex is neither). Objects embed one of these to be checked, and Stroika objects are
119 * copy-by-value, so the checker has to copy and move along with its owner. See the static_assert
120 * at the bottom of this file for the full rationale.
121 *
122 * \par Example Usage
123 * \code
124 * struct foo {
125 * qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS Debug::AssertExternallySynchronizedChecker fThisAssertExternallySynchronized_;
126 * inline void DoReadWriteStuffOnData ()
127 * {
128 * AssertExternallySynchronizedChecker::WriteContext declareContext { fThisAssertExternallySynchronized_ };
129 * // now do what you usually do for to modify locked data...
130 * }
131 * inline void DoReadOnlyStuffOnData ()
132 * {
133 * AssertExternallySynchronizedChecker::ReadContext declareContext { fThisAssertExternallySynchronized_ };
134 * // now do what you usually do for DoReadOnlyStuffOnData - reading data only...
135 * }
136 * };
137 * \endcode
138 *
139 * \par Example Usage
140 * \code
141 * // this style of use - subclassing - is especially useful if the object foo will be subclassed, and checked throughout the
142 * // code (or subclasses) with Debug::AssertExternallySynchronizedChecker::ReadContext (or WriteContext)
143 * struct foo : public Debug::AssertExternallySynchronizedChecker {
144 * inline void DoReadWriteStuffOnData ()
145 * {
146 * AssertExternallySynchronizedChecker::WriteContext declareContext { *this }; // lock_guard or scopedLock or unique_lock
147 * // now do what you usually do for to modify locked data...
148 * }
149 * inline void DoReadOnlyStuffOnData ()
150 * {
151 * AssertExternallySynchronizedChecker::ReadContext declareContext { *this };
152 * // now do what you usually do for DoReadOnlyStuffOnData - reading data only...
153 * }
154 * };
155 * \endcode
156 *
157 * \note This is SUPER-RECURSIVE lock. It allows lock() when shared_lock held (by only this thread) - so upgrades the lock.
158 * And it allows shared_lock when lock held by the same thread. Otherwise it asserts when a thread conflict is found.
159 * lock() and shared_lock () - here - are NEVER blocking. They just assert there is no conflict.
160 */
162#if qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled
163 public:
164 /**
165 * Explicit shared context object, so we can construct multiple AssertExternallySynchronizedChecker which all
166 * share a common 'sharedContext' - representing that they ALL must be externally synchronized across all the cooperating objects
167 *
168 * In most cases, just ignore this class.
169 *
170 * To have N cooperating classes (e.g. object, and a few direct members) all share the same rules of single-threading (treating them all
171 * as one object for the purpose of the rules of safe multithread access) - arrange for them to share a common 'sharedContext'
172 *
173 * \note class marked final to make more clear why safe to not have virtual destructor
174 */
175 struct SharedContext final {
176 public:
177 SharedContext () noexcept = default;
178 SharedContext (const SharedContext&) = delete;
179 SharedContext& operator= (const SharedContext&) = delete;
180 ~SharedContext ();
181
182 private:
183 atomic_uint_fast32_t fFullLocks_{0};
184 thread::id fThreadWithFullLock_; // or value undefined/last value where it had full lock
185
186 private:
187 // Use of inline array avoids mallocs, and makes this run slightly faster. No semantic differerence,
188 // just makes debug mode a bit faster.
189 static constexpr size_t kInlineSharedLockBufSize_ = 2;
190 struct {
191 // most logically a multiset, but std::multiset is not threadsafe and requires external locking.
192 // So does forward_list, but its closer to lock free, so try it for now
193 // GetSharedLockMutexThreads_ () used to access fSharedLocks_
194 array<thread::id, kInlineSharedLockBufSize_> fInitialThreads_;
195 uint8_t fInitialThreadsSize_{0}; // not sure how to add this field only conditionally
196 forward_list<thread::id> fOverflowThreads_;
197 } fSharedLocks_;
198
199 private:
200 bool GetSharedLockEmpty_ () const;
201 pair<size_t, size_t> CountSharedLockThreads_ () const;
202 size_t GetSharedLockThreadsCount_ () const;
203 size_t CountOfIInSharedLockThreads_ (thread::id i) const;
204 void AddSharedLock_ (thread::id i);
205 void RemoveSharedLock_ (thread::id i);
206
207 private:
209 };
210#endif
211
212 public:
213 /**
214 * \note Copy/Move constructor checks for existing locks while copying.
215 * Must be able to read lock source on copy, and have zero existing locks on src for move.
216 * These 'constructors' don't really do/copy/move anything, but just check the state of their own
217 * lock count and the state of the 'src' lock counts.
218 *
219 * NOTE - the 'SharedContext' does NOT get copied by copy constructors, move constructors etc. Its tied
220 * to the l-value.
221 */
222#if qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled
223 AssertExternallySynchronizedChecker (const shared_ptr<SharedContext>& sharedContext = nullptr) noexcept;
224 AssertExternallySynchronizedChecker (const shared_ptr<SharedContext>& sharedContext, AssertExternallySynchronizedChecker&& src) noexcept;
227 AssertExternallySynchronizedChecker (const shared_ptr<SharedContext>& sharedContext, const AssertExternallySynchronizedChecker& src) noexcept;
228#else
229 constexpr AssertExternallySynchronizedChecker () noexcept = default;
232#endif
233
234 public:
235 /**
236 * \note operator= checks for existing locks while copying.
237 * Must be able to read lock source on copy, and have zero existing locks on target or move.
238 */
241
242#if qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled
243 public:
244 nonvirtual shared_ptr<SharedContext> GetSharedContext () const;
245
246 public:
247 /**
248 * Make it easy for subclasses to expose SetAssertExternallySynchronizedCheckerContext () functionality, so those
249 * subclasses can allow users of those classes to share a sharing context.
250 *
251 * \note - this is named without the prefixing '_' (though protected) to make it easier to forward, just using using.
252 */
253 nonvirtual void SetAssertExternallySynchronizedCheckerContext (const shared_ptr<SharedContext>& sharedContext);
254#endif
255
256 public:
257 /**
258 * Saves current thread, and increments lock count, and
259 * \pre already locked by this thread or no existing locks (either shared or exclusive)
260 *
261 * \note method non-const (can always const_cast if needed) because of standard C++ convention of non-const objects
262 * for write-lock
263 */
264 nonvirtual void lock () noexcept;
265
266 public:
267 /**
268 * \brief Like lock() - if it would succeed, same then, but if would fail instead of assert out, just return false.
269 */
270 nonvirtual bool try_lock () noexcept;
271
272 public:
273 /**
274 * Just decrement lock count
275 *
276 * \pre still running on the same locking thread and locks not unbalanced
277 */
278 nonvirtual void unlock () noexcept;
279
280 public:
281 /**
282 * Saves current thread (multiset), and increments shared count, and
283 * \pre no pre-existing locks on other threads
284 *
285 * \note method const despite usual lockable rules, so easier to work with 'const' objects being 'marked' as doing a read operation.
286 */
287 nonvirtual void lock_shared () const noexcept;
288
289 public:
290 /**
291 * Just decrement shared lock count (remove this thread from shared lock multiset)
292 *
293 * \note see lock_shard for why const.
294 *
295 * \pre still running on the same locking thread and locks not unbalanced
296 */
297 nonvirtual void unlock_shared () const noexcept;
298
299 public:
300 /**
301 * \brief Instantiate AssertExternallySynchronizedChecker::ReadContext to designate an area of code where protected data will be read
302 *
303 * This type alias makes a little more clear in reading code that the 'lock' is really just an assertion about thread safety
304 *
305 * Since AssertExternallySynchronizedChecker follows the concept 'mutex' you can obviously use any
306 * of the standard lockers in std::c++, but using AssertExternallySynchronizedChecker::ReadContext - makes it a little more clear
307 * self-documenting in your code, that you are doing this in a context where you are only reading the pseudo-locked data.
308 *
309 * \note we get away with 'const' in shared_lock<const AssertExternallySynchronizedChecker> because we chose to make
310 * lock_shared, and unlock_shared const methods (see their docs above).
311 *
312 * \note - though CTOR not declared noexcept, ReadContext cannot throw an exception (it asserts out on failure)
313 */
315 static_assert (movable<ReadContext> and not copyable<ReadContext>);
316
317 public:
318 /**
319 * \brief Instantiate AssertExternallySynchronizedChecker::WriteContext to designate an area of code where protected data will be written
320 *
321 * This type alias makes a little more clear in reading code that the 'lock' is really just an assertion about thread safety
322 *
323 * Since AssertExternallySynchronizedChecker follows the concept 'mutex' you can obviously use any
324 * of the standard lockers in std::c++, but using AssertExternallySynchronizedChecker::WriteContext - makes it a little more clear
325 * self-documenting in your code, that you are doing this in a context where you are only writing the pseudo-locked data.
326 *
327 * Plus, the fact that it forces a non-const interpretation on the object in question (by using lock_guard of a non-const AssertExternallySynchronizedChecker)
328 * makes it a little easier to catch cases where you accidentally use WriteContext and meant ReadContext.
329 *
330 * \note - used lock_guard before Stroika v3.0d10, but switched to unique_lock so movable (handy in some cases).
331 * And performance not much of an issue since this is all debug-only code.
332 *
333 * \note - though CTOR not declared noexcept, WriteContext cannot throw an exception (it asserts out on failure)
334 */
336 static_assert (movable<WriteContext> and not copyable<WriteContext>);
337
338#if qStroika_Foundation_Debug_AssertExternallySynchronizedChecker_Enabled
339 private:
340 nonvirtual void lock_ () noexcept;
341 nonvirtual bool try_lock_ () noexcept;
342 nonvirtual void unlock_ () noexcept;
343 nonvirtual void lock_shared_ () const noexcept;
344 nonvirtual void unlock_shared_ () const noexcept;
345
346 private:
347 shared_ptr<SharedContext> fSharedContext_;
348
349 private:
350 static mutex& GetSharedLockMutexThreads_ (); // MUTEX ONLY FOR fSharedLocks_ (could do one mutex per AssertExternallySynchronizedChecker but static probably performs better)
351#endif
352 };
353 /*
354 * See 'Satisfies Concepts' in the class docs above.
355 *
356 * The copyable/movable half is DELIBERATE and load-bearing - do not "correct" it to match std::mutex,
357 * which is neither copyable nor movable. That difference is not an oversight, it follows from this not
358 * being a mutex: it is a checker that objects EMBED as a data member (or inherit from). Stroika is a
359 * copy-by-value framework - String, Sequence<T>, Mapping<T> and friends must stay copyable and movable -
360 * so anything they embed must be too. Deleting these operations here would make every object holding one
361 * non-copyable, which is ~181 files at last count.
362 *
363 * What copy/move actually do is check, not transfer: no lock state is carried across, and the
364 * SharedContext stays tied to the l-value. They assert the source (and for move, the target) is not
365 * locked out from under the operation - see the constructor and operator= notes above.
366 */
367 static_assert (movable<AssertExternallySynchronizedChecker> and copyable<AssertExternallySynchronizedChecker> and
369 static_assert (not movable<mutex> and not copyable<mutex>); // the contrast above, made checkable
370
371}
372
373/*
374 ********************************************************************************
375 ***************************** Implementation Details ***************************
376 ********************************************************************************
377 */
378#include "AssertExternallySynchronizedChecker.inl"
379
380#endif /*_Stroika_Foundation_Debug_AssertExternallySynchronizedChecker_h_*/
NOT a real mutex - just a debugging infrastructure support tool so in debug builds can be assured thr...
unique_lock< AssertExternallySynchronizedChecker > WriteContext
Instantiate AssertExternallySynchronizedChecker::WriteContext to designate an area of code where prot...
nonvirtual AssertExternallySynchronizedChecker & operator=(AssertExternallySynchronizedChecker &&rhs) noexcept
nonvirtual bool try_lock() noexcept
Like lock() - if it would succeed, same then, but if would fail instead of assert out,...
shared_lock< const AssertExternallySynchronizedChecker > ReadContext
Instantiate AssertExternallySynchronizedChecker::ReadContext to designate an area of code where prote...
Logically the C++ standard Lockable named requirement, but that was not included in std c++ library.
Definition StdCompat.h:77