1 //  Copyright (c) 2011-present, Facebook, Inc.  All rights reserved.
2 //  This source code is licensed under both the GPLv2 (found in the
3 //  COPYING file in the root directory) and Apache 2.0 License
4 //  (found in the LICENSE.Apache file in the root directory).
5 
6 #pragma once
7 
8 #include <atomic>
9 #include <chrono>
10 #include <cstdint>
11 
12 namespace folly {
13 namespace detail {
14 namespace distributed_mutex {
15 
16 /**
17  * DistributedMutex is a small, exclusive-only mutex that distributes the
18  * bookkeeping required for mutual exclusion in the stacks of threads that are
19  * contending for it.  It has a mode that can combine critical sections when
20  * the mutex experiences contention; this allows the implementation to elide
21  * several expensive coherence and synchronization operations to boost
22  * throughput, surpassing even atomic instructions in some cases.  It has a
23  * smaller memory footprint than std::mutex, a similar level of fairness
24  * (better in some cases) and no dependencies on heap allocation.  It is the
25  * same width as a single pointer (8 bytes on most platforms), where on the
26  * other hand, std::mutex and pthread_mutex_t are both 40 bytes.  It is larger
27  * than some of the other smaller locks, but the wide majority of cases using
28  * the small locks are wasting the difference in alignment padding anyway
29  *
30  * Benchmark results are good - at the time of writing, in the contended case,
31  * for lock/unlock based critical sections, it is about 4-5x faster than the
32  * smaller locks and about ~2x faster than std::mutex.  When used in
33  * combinable mode, it is much faster than the alternatives, going more than
34  * 10x faster than the small locks, about 6x faster than std::mutex, 2-3x
35  * faster than flat combining and even faster than std::atomic<> in some
36  * cases, allowing more work with higher throughput.  In the uncontended case,
37  * it is a few cycles faster than folly::MicroLock but a bit slower than
38  * std::mutex.  DistributedMutex is also resistent to tail latency pathalogies
39  * unlike many of the other mutexes in use, which sleep for large time
40  * quantums to reduce spin churn, this causes elevated latencies for threads
41  * that enter the sleep cycle.  The tail latency of lock acquisition can go up
42  * to 10x lower because of a more deterministic scheduling algorithm that is
43  * managed almost entirely in userspace.  Detailed results comparing the
44  * throughput and latencies of different mutex implementations and atomics are
45  * at the bottom of folly/synchronization/test/SmallLocksBenchmark.cpp
46  *
47  * Theoretically, write locks promote concurrency when the critical sections
48  * are small as most of the work is done outside the lock.  And indeed,
49  * performant concurrent applications go through several pains to limit the
50  * amount of work they do while holding a lock.  However, most times, the
51  * synchronization and scheduling overhead of a write lock in the critical
52  * path is so high, that after a certain point, making critical sections
53  * smaller does not actually increase the concurrency of the application and
54  * throughput plateaus.  DistributedMutex moves this breaking point to the
55  * level of hardware atomic instructions, so applications keep getting
56  * concurrency even under very high contention.  It does this by reducing
57  * cache misses and contention in userspace and in the kernel by making each
58  * thread wait on a thread local node and futex.  When combined critical
59  * sections are used DistributedMutex leverages template metaprogramming to
60  * allow the mutex to make better synchronization decisions based on the
61  * layout of the input and output data.  This allows threads to keep working
62  * only on their own cache lines without requiring cache coherence operations
63  * when a mutex experiences heavy contention
64  *
65  * Non-timed mutex acquisitions are scheduled through intrusive LIFO
66  * contention chains.  Each thread starts by spinning for a short quantum and
67  * falls back to two phased sleeping.  Enqueue operations are lock free and
68  * are piggybacked off mutex acquisition attempts.  The LIFO behavior of a
69  * contention chain is good in the case where the mutex is held for a short
70  * amount of time, as the head of the chain is likely to not have slept on
71  * futex() after exhausting its spin quantum.  This allow us to avoid
72  * unnecessary traversal and syscalls in the fast path with a higher
73  * probability.  Even though the contention chains are LIFO, the mutex itself
74  * does not adhere to that scheduling policy globally.  During contention,
75  * threads that fail to lock the mutex form a LIFO chain on the central mutex
76  * state, this chain is broken when a wakeup is scheduled, and future enqueue
77  * operations form a new chain.  This makes the chains themselves LIFO, but
78  * preserves global fairness through a constant factor which is limited to the
79  * number of concurrent failed mutex acquisition attempts.  This binds the
80  * last in first out behavior to the number of contending threads and helps
81  * prevent starvation and latency outliers
82  *
83  * This strategy of waking up wakers one by one in a queue does not scale well
84  * when the number of threads goes past the number of cores.  At which point
85  * preemption causes elevated lock acquisition latencies.  DistributedMutex
86  * implements a hardware timestamp publishing heuristic to detect and adapt to
87  * preemption.
88  *
89  * DistributedMutex does not have the typical mutex API - it does not satisfy
90  * the Lockable concept.  It requires the user to maintain ephemeral bookkeeping
91  * and pass that bookkeeping around to unlock() calls.  The API overhead,
92  * however, comes for free when you wrap this mutex for usage with
93  * std::unique_lock, which is the recommended usage (std::lock_guard, in
94  * optimized mode, has no performance benefit over std::unique_lock, so has been
95  * omitted).  A benefit of this API is that it disallows incorrect usage where a
96  * thread unlocks a mutex that it does not own, thinking a mutex is functionally
97  * identical to a binary semaphore, which, unlike a mutex, is a suitable
98  * primitive for that usage
99  *
100  * Combined critical sections allow the implementation to elide several
101  * expensive operations during the lifetime of a critical section that cause
102  * slowdowns with regular lock/unlock based usage.  DistributedMutex resolves
103  * contention through combining up to a constant factor of 2 contention chains
104  * to prevent issues with fairness and latency outliers, so we retain the
105  * fairness benefits of the lock/unlock implementation with no noticeable
106  * regression when switching between the lock methods.  Despite the efficiency
107  * benefits, combined critical sections can only be used when the critical
108  * section does not depend on thread local state and does not introduce new
109  * dependencies between threads when the critical section gets combined.  For
110  * example, locking or unlocking an unrelated mutex in a combined critical
111  * section might lead to unexpected results or even undefined behavior.  This
112  * can happen if, for example, a different thread unlocks a mutex locked by
113  * the calling thread, leading to undefined behavior as the mutex might not
114  * allow locking and unlocking from unrelated threads (the posix and C++
115  * standard disallow this usage for their mutexes)
116  *
117  * Timed locking through DistributedMutex is implemented through a centralized
118  * algorithm.  The underlying contention-chains framework used in
119  * DistributedMutex is not abortable so we build abortability on the side.
120  * All waiters wait on the central mutex state, by setting and resetting bits
121  * within the pointer-length word.  Since pointer length atomic integers are
122  * incompatible with futex(FUTEX_WAIT) on most systems, a non-standard
123  * implementation of futex() is used, where wait queues are managed in
124  * user-space (see p1135r0 and folly::ParkingLot for more)
125  */
126 template <
127     template <typename> class Atomic = std::atomic,
128     bool TimePublishing = true>
129 class DistributedMutex {
130  public:
131   class DistributedMutexStateProxy;
132 
133   /**
134    * DistributedMutex is only default constructible, it can neither be moved
135    * nor copied
136    */
137   DistributedMutex();
138   DistributedMutex(DistributedMutex&&) = delete;
139   DistributedMutex(const DistributedMutex&) = delete;
140   DistributedMutex& operator=(DistributedMutex&&) = delete;
141   DistributedMutex& operator=(const DistributedMutex&) = delete;
142 
143   /**
144    * Acquires the mutex in exclusive mode
145    *
146    * This returns an ephemeral proxy that contains internal mutex state.  This
147    * must be kept around for the duration of the critical section and passed
148    * subsequently to unlock() as an rvalue
149    *
150    * The proxy has no public API and is intended to be for internal usage only
151    *
152    * There are three notable cases where this method causes undefined
153    * behavior:
154    *
155    *  - This is not a recursive mutex.  Trying to acquire the mutex twice from
156    *    the same thread without unlocking it results in undefined behavior
157    *  - Thread, coroutine or fiber migrations from within a critical section
158    *    are disallowed.  This is because the implementation requires owning the
159    *    stack frame through the execution of the critical section for both
160    *    lock/unlock or combined critical sections.  This also means that you
161    *    cannot allow another thread, fiber or coroutine to unlock the mutex
162    *  - This mutex cannot be used in a program compiled with segmented stacks,
163    *    there is currently no way to detect the presence of segmented stacks
164    *    at compile time or runtime, so we have no checks against this
165    */
166   DistributedMutexStateProxy lock();
167 
168   /**
169    * Unlocks the mutex
170    *
171    * The proxy returned by lock must be passed to unlock as an rvalue.  No
172    * other option is possible here, since the proxy is only movable and not
173    * copyable
174    *
175    * It is undefined behavior to unlock from a thread that did not lock the
176    * mutex
177    */
178   void unlock(DistributedMutexStateProxy);
179 
180   /**
181    * Try to acquire the mutex
182    *
183    * A non blocking version of the lock() function.  The returned object is
184    * contextually convertible to bool.  And has the value true when the mutex
185    * was successfully acquired, false otherwise
186    *
187    * This is allowed to return false spuriously, i.e. this is not guaranteed
188    * to return true even when the mutex is currently unlocked.  In the event
189    * of a failed acquisition, this does not impose any memory ordering
190    * constraints for other threads
191    */
192   DistributedMutexStateProxy try_lock();
193 
194   /**
195    * Try to acquire the mutex, blocking for the given time
196    *
197    * Like try_lock(), this is allowed to fail spuriously and is not guaranteed
198    * to return false even when the mutex is currently unlocked.  But only
199    * after the given time has elapsed
200    *
201    * try_lock_for() accepts a duration to block for, and try_lock_until()
202    * accepts an absolute wall clock time point
203    */
204   template <typename Rep, typename Period>
205   DistributedMutexStateProxy try_lock_for(
206       const std::chrono::duration<Rep, Period>& duration);
207 
208   /**
209    * Try to acquire the lock, blocking until the given deadline
210    *
211    * Other than the difference in the meaning of the second argument, the
212    * semantics of this function are identical to try_lock_for()
213    */
214   template <typename Clock, typename Duration>
215   DistributedMutexStateProxy try_lock_until(
216       const std::chrono::time_point<Clock, Duration>& deadline);
217 
218   /**
219    * Execute a task as a combined critical section
220    *
221    * Unlike traditional lock and unlock methods, lock_combine() enqueues the
222    * passed task for execution on any arbitrary thread.  This allows the
223    * implementation to prevent cache line invalidations originating from
224    * expensive synchronization operations.  The thread holding the lock is
225    * allowed to execute the task before unlocking, thereby forming a "combined
226    * critical section".
227    *
228    * This idea is inspired by Flat Combining.  Flat Combining was introduced
229    * in the SPAA 2010 paper titled "Flat Combining and the
230    * Synchronization-Parallelism Tradeoff", by Danny Hendler, Itai Incze, Nir
231    * Shavit, and Moran Tzafrir -
232    * https://www.cs.bgu.ac.il/~hendlerd/papers/flat-combining.pdf.  The
233    * implementation used here is significantly different from that described
234    * in the paper.  The high-level goal of reducing the overhead of
235    * synchronization, however, is the same.
236    *
237    * Combined critical sections work best when kept simple.  Since the
238    * critical section might be executed on any arbitrary thread, relying on
239    * things like thread local state or mutex locking and unlocking might cause
240    * incorrectness.  Associativity is important.  For example
241    *
242    *    auto one = std::unique_lock{one_};
243    *    two_.lock_combine([&]() {
244    *      if (bar()) {
245    *        one.unlock();
246    *      }
247    *    });
248    *
249    * This has the potential to cause undefined behavior because mutexes are
250    * only meant to be acquired and released from the owning thread.  Similar
251    * errors can arise from a combined critical section introducing implicit
252    * dependencies based on the state of the combining thread.  For example
253    *
254    *    // thread 1
255    *    auto one = std::unique_lock{one_};
256    *    auto two = std::unique_lock{two_};
257    *
258    *    // thread 2
259    *    two_.lock_combine([&]() {
260    *      auto three = std::unique_lock{three_};
261    *    });
262    *
263    * Here, because we used a combined critical section, we have introduced a
264    * dependency from one -> three that might not obvious to the reader
265    *
266    * This function is exception-safe.  If the passed task throws an exception,
267    * it will be propagated to the caller, even if the task is running on
268    * another thread
269    *
270    * There are three notable cases where this method causes undefined
271    * behavior:
272    *
273    *  - This is not a recursive mutex.  Trying to acquire the mutex twice from
274    *    the same thread without unlocking it results in undefined behavior
275    *  - Thread, coroutine or fiber migrations from within a critical section
276    *    are disallowed.  This is because the implementation requires owning the
277    *    stack frame through the execution of the critical section for both
278    *    lock/unlock or combined critical sections.  This also means that you
279    *    cannot allow another thread, fiber or coroutine to unlock the mutex
280    *  - This mutex cannot be used in a program compiled with segmented stacks,
281    *    there is currently no way to detect the presence of segmented stacks
282    *    at compile time or runtime, so we have no checks against this
283    */
284   template <typename Task>
285   auto lock_combine(Task task) -> decltype(std::declval<const Task&>()());
286 
287  private:
288   Atomic<std::uintptr_t> state_{0};
289 };
290 
291 } // namespace distributed_mutex
292 } // namespace detail
293 
294 /**
295  * Bring the default instantiation of DistributedMutex into the folly
296  * namespace without requiring any template arguments for public usage
297  */
298 extern template class detail::distributed_mutex::DistributedMutex<>;
299 using DistributedMutex = detail::distributed_mutex::DistributedMutex<>;
300 
301 } // namespace folly
302 
303 #include <folly/synchronization/DistributedMutex-inl.h>
304 #include <folly/synchronization/DistributedMutexSpecializations.h>
305