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