Stroika Library 3.0d25
 
Loading...
Searching...
No Matches
SpinLock.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_SpinLock_h_
5#define _Stroika_Foundation_Execution_SpinLock_h_ 1
6
7#include "Stroika/Foundation/StroikaPreComp.h"
8
9#include <atomic>
10
11#include "Stroika/Foundation/Common/Common.h"
14
15/**
16 * \file
17 */
18
20
21 /**
22 * SpinLock and mutex can be nearly used interchangeably. Oftentimes, users will want to define a typedef which selects
23 * the faster implementation.
24 *
25 * How much faster than a mutex? Tests/52 carries that comparison as a threshold test.
26 * Untrustworthy (not super well tested) results maybe still helpful:
27 *
28 * +--------------+------------+----------------------+-----------+-------+
29 * | OS | CPU | compiler | std lib | ratio |
30 * +--------------+------------+----------------------+-----------+-------+
31 * | Windows | x86-64 | MSVC 14.51.36231 | MSVC STL | 0.49 |
32 * | macOS 26.5 | arm64 (M1) | Apple clang 17.0.0 | libc++ | 0.72 |
33 * | Ubuntu 26.04 | x86-64 | g++ 16 | libstdc++ | 1.22 |
34 * | Ubuntu 26.04 | x86-64 | g++ 15 (C++23) | libstdc++ | 1.25 |
35 * +--------------+------------+----------------------+-----------+-------+
36 * ratio = SpinLock time / mutex time, so BELOW 1.0 means SpinLock won. Tests/52 at
37 * -x 15, Release builds, pinned to one core where the OS allows it (macOS does not).
38 *
39 * There is no clang Release configuration to run Tests/52 from, so the compiler-versus-standard-
40 * library question was separated with a standalone replica of the same loop, all three built and
41 * run on one Linux box:
42 *
43 * g++ 16 + libstdc++ 1.57 SpinLock's own time came out the SAME in all
44 * clang++ 22 + libstdc++ 1.25 three (1.554 / 1.554 / 1.556s) - every bit of
45 * clang++ 22 + libc++ 0.99 the spread is in how fast std::mutex is.
46 *
47 * So this tracks the standard library rather than the compiler: libstdc++ has a very fast
48 * uncontended std::mutex and beats this spinlock whichever compiler builds it, libc++ on Linux
49 * is a wash, and MSVC STL and libc++-on-macOS lose to it clearly. Which is why
50 * kSpinLock_IsFasterThan_mutex below keys off the std-library macros. Beware comparing the two
51 * tables: the replica put g++/libstdc++ at 1.57 where Tests/52 said 1.22, so trust the SIGN of
52 * any of this and not the magnitude.
53 *
54 * ***And read this caveat before believing any of it.*** That loop takes and releases the lock from a
55 * SINGLE thread, which is a spinlock's best case: no syscall, no futex, and never any actual waiting.
56 * Under real contention a spinlock burns CPU while it spins, and can be very much WORSE than a mutex -
57 * so treat these numbers as the ceiling rather than the expected value, and measure your own case if
58 * it is contended.
59 *
60 * \note Stroika 2.0a155 and earlier - Execution::kSpinLock_IsFasterThan_mutex was always true
61 *
62 * \note Stroika 2.0a156 through 3.0d24 - due to threadFence and
63 * https://github.com/SophistSolutions/Stroika/issues/628 (STK-494) - SpinLock slowed slightly, but its
64 * still notably faster than mutex on gcc/unix/windows (x86 only tested).
65 *
66 * \note Stroika 3.0d25 and later - the standalone fences, and the BarrierType knob that selected among
67 * them, were both removed (@see SpinLock::try_lock ()). Careful about attributing the resulting
68 * speedup: on x86-64 those fences emitted NO instruction at all (an acquire or release fence is only
69 * a compiler barrier there), so what actually shrank the generated code was dropping the runtime
70 * barrier dispatch - try_lock () went 11 instructions to 5, unlock () 8 to 3. The fences still
71 * mattered, just not for speed on x86: ThreadSanitizer cannot model them, and on weakly ordered
72 * hardware they are real instructions.
73 */
75#if defined(_GLIBCXX_RELEASE)
76 false // libstdc++ (whichever compiler builds it): measured 1.22 .. 1.57 - its uncontended std::mutex wins
77#elif defined(_LIBCPP_VERSION) and qStroika_Platform_MacOS
78 true // libc++ on macOS/arm64: measured 0.72
79#elif defined(_LIBCPP_VERSION)
80 false // libc++ elsewhere (Linux): measured 0.99, a tie - so prefer mutex, which degrades better under contention
81#elif defined(_MSVC_STL_VERSION)
82 true // MSVC STL: measured 0.49
83#else
84 false // unmeasured std library - same tie-break: prefer mutex
85#endif
86 ;
87
88 /**
89 * Implementation based on
90 * http://en.cppreference.com/w/cpp/atomic/atomic_flag
91 *
92 * \note - SpinLock - generally faster than most mutexes for short accesses (see kSpinLock_IsFasterThan_mutex)
93 * BUT it can be degererate (due to spinning) and take LONGER if used to hold locks a long time.
94 *
95 * \note - not recursive mutex
96 *
97 * \note Satisfies Concepts:
98 * o Common::StdCompat::BasicLockable<SpinLock>
99 * o Common::StdCompat::Lockable<SpinLock>
100 * o not movable<SpinLock>, not copyable<SpinLock> (as std::mutex)
101 *
102 * \note ***Not Cancelation Point***
103 * SpinLock contains no cancelation points, and so can be used interchangeably with mutex - just for the performance differences
104 *
105 * \note ***Memory ordering***
106 * You use a mutex to protect some data structure. A thread that acquires the lock needs to see the writes
107 * made by whoever held it last (that is the acquire), and a thread that releases the lock needs its own
108 * writes published to whoever acquires it next (that is the release). So acquire on the test_and_set ()
109 * and release on the clear () is exactly - and entirely - what a mutex owes its caller.
110 *
111 * That is not a preference, it is the contract: [thread.mutex.requirements.mutex] asks a mutex for just
112 * two things - that lock/unlock on a SINGLE mutex appear in one total order, which the modification order
113 * of the flag already provides at any memory order, and that "prior unlock () operations on the same
114 * object shall synchronize with this operation", which is precisely release paired with acquire. Nothing
115 * weaker satisfies it and nothing stronger adds to it. @see try_lock ().
116 *
117 * \note v3.0d25 removed the BarrierType constructor argument (eNoBarrier / eReleaseAcquire / eMemoryTotalOrder),
118 * which had let a caller pick a different memory order for the flag. It went because neither alternative
119 * to the default was defensible:
120 * o eNoBarrier never had an observable effect - the flag was read acquire and cleared release whatever
121 * you passed - and had it been made to mean memory_order_relaxed it would have been actively unsafe.
122 * Mutual exclusion would survive (RMW atomicity does not depend on memory order), but the protected
123 * data would no longer be published to the next holder, and the compiler would be free to move that
124 * data's accesses across the lock and the unlock. It would not even have been faster on x86-64,
125 * where test_and_set () emits the same LOCK-prefixed xchg for relaxed as for acquire, and clear ()
126 * the same plain store for relaxed as for release.
127 * o eMemoryTotalOrder (memory_order_seq_cst) is distinguishable only by protocols reasoning about the
128 * global order of operations on SEVERAL DIFFERENT locks. The standard deliberately promises a total
129 * order per mutex and not across mutexes, so that is not a mutex guarantee anywhere - offering it
130 * here would have made SpinLock advertise something std::mutex does not, against the whole premise
131 * that the two are interchangeable. Code that truly needs it wants a seq_cst atomic, named as such
132 * at the point of use.
133 *
134 * \note References, in the order they became useful:
135 * o https://github.com/SophistSolutions/Stroika/issues/628 (STK-494) - where the reasoning above was
136 * worked out and settled (see the closing 2018-01-27 comment)
137 * o https://www.boost.org/doc/libs/1_54_0/doc/html/atomic/usage_examples.html#boost_atomic.usage_examples.example_spinlock
138 * o https://blog.regehr.org/archives/2173 - alleges that done properly you do not need the fence.
139 * That turned out to be right. It also suggests a fairer spinlock, which this still is not.
140 */
141 class SpinLock {
142 public:
143 SpinLock () = default;
144 SpinLock (const SpinLock&) = delete;
145
146 public:
147 ~SpinLock () = default;
148
149 public:
150 nonvirtual SpinLock& operator= (const SpinLock&) = delete;
151
152 public:
153 /**
154 */
155 nonvirtual bool try_lock ();
156
157 public:
158 /**
159 */
160 nonvirtual void lock ();
161
162 public:
163 /**
164 */
165 nonvirtual void unlock ();
166
167 private:
168 atomic_flag fLock_;
169 };
170
171 /*
172 * See 'Satisfies Concepts' in the class docs above.
173 */
175 static_assert (not movable<SpinLock> and not copyable<SpinLock>); // like std::mutex
176
177}
178
179/*
180 ********************************************************************************
181 ***************************** Implementation Details ***************************
182 ********************************************************************************
183 */
184#include "SpinLock.inl"
185
186#endif /*_Stroika_Foundation_Execution_SpinLock_h_*/
Logically the C++ standard BasicLockable named requirement, but that was not included in std c++ libr...
Definition StdCompat.h:77
Logically the C++ standard Lockable named requirement, but that was not included in std c++ library.
Definition StdCompat.h:96
constexpr bool kSpinLock_IsFasterThan_mutex
Definition SpinLock.h:74