/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // // Docs: https://fburl.com/fbcref_synchronized // /** * This module implements a Synchronized abstraction useful in * mutex-based concurrency. * * The Synchronized<T, Mutex> class is the primary public API exposed by this * module. See folly/docs/Synchronized.md for a more complete explanation of * this class and its benefits. */ #pragma once #include <folly/Function.h> #include <folly/Likely.h> #include <folly/Preprocessor.h> #include <folly/SharedMutex.h> #include <folly/Traits.h> #include <folly/Utility.h> #include <folly/container/Foreach.h> #include <folly/functional/ApplyTuple.h> #include <folly/synchronization/Lock.h> #include <glog/logging.h> #include <array> #include <mutex> #include <tuple> #include <type_traits> #include <utility> namespace folly { namespace detail { kSynchronizedMutexIsUnique; kSynchronizedMutexIsUnique; kSynchronizedMutexIsShared; kSynchronizedMutexIsShared; kSynchronizedMutexIsUpgrade; kSynchronizedMutexIsUpgrade; /** * An enum to describe the "level" of a mutex. The supported levels are * Unique - a normal mutex that supports only exclusive locking * Shared - a shared mutex which has shared locking and unlocking functions; * Upgrade - a mutex that has all the methods of the two above along with * support for upgradable locking */ enum class SynchronizedMutexLevel { … }; kSynchronizedMutexLevel; enum class SynchronizedMutexMethod { … }; template <SynchronizedMutexLevel Level, SynchronizedMutexMethod Method> struct SynchronizedLockPolicy { … }; SynchronizedLockPolicyExclusive; SynchronizedLockPolicyTryExclusive; SynchronizedLockPolicyShared; SynchronizedLockPolicyTryShared; SynchronizedLockPolicyUpgrade; SynchronizedLockPolicyTryUpgrade; template <SynchronizedMutexLevel> struct SynchronizedLockType_ { … }; template <> struct SynchronizedLockType_<SynchronizedMutexLevel::Unique> { … }; template <> struct SynchronizedLockType_<SynchronizedMutexLevel::Shared> { … }; template <> struct SynchronizedLockType_<SynchronizedMutexLevel::Upgrade> { … }; SynchronizedLockType; } // namespace detail /** * SynchronizedBase is a helper parent class for Synchronized<T>. * * It provides wlock() and rlock() methods for shared mutex types, * or lock() methods for purely exclusive mutex types. */ template <class Subclass, detail::SynchronizedMutexLevel level> class SynchronizedBase; template <class LockedType, class Mutex, class LockPolicy> class LockedPtrBase; template <class LockedType, class LockPolicy> class LockedPtr; /** * SynchronizedBase specialization for shared mutex types. * * This class provides wlock() and rlock() methods for acquiring the lock and * accessing the data. */ SynchronizedBase<Subclass, detail::SynchronizedMutexLevel::Shared>; /** * SynchronizedBase specialization for upgrade mutex types. * * This class provides all the functionality provided by the SynchronizedBase * specialization for shared mutexes and a ulock() method that returns an * upgrade lock RAII proxy */ SynchronizedBase<Subclass, detail::SynchronizedMutexLevel::Upgrade>; /** * SynchronizedBase specialization for non-shared mutex types. * * This class provides lock() methods for acquiring the lock and accessing the * data. */ SynchronizedBase<Subclass, detail::SynchronizedMutexLevel::Unique>; /** * `folly::Synchronized` pairs a datum with a mutex. The datum can only be * reached through a `LockedPtr`, typically acquired via `.rlock()` or * `.wlock()`; the mutex is held for the lifetime of the `LockedPtr`. * * It is recommended to explicitly open a new nested scope when aquiring * a `LockedPtr` object, to help visibly delineate the critical section and to * ensure that the `LockedPtr` is destroyed as soon as it is no longer needed. * * @tparam T The type of datum to be stored. * @tparam Mutex The mutex type that guards the datum. Must be Lockable. * * @refcode folly/docs/examples/folly/Synchronized.cpp */ template <class T, class Mutex = SharedMutex> struct Synchronized : public SynchronizedBase< Synchronized<T, Mutex>, detail::kSynchronizedMutexLevel<Mutex>> { … }; /** * Deprecated subclass of Synchronized that provides implicit locking * via operator->. This is intended to ease migration while preventing * accidental use of operator-> in new code. */ template <class T, class Mutex = SharedMutex> struct [[deprecated( "use Synchronized and explicit lock(), wlock(), or rlock() instead")]] ImplicitSynchronized : Synchronized<T, Mutex> { … }; template <class SynchronizedType, class LockPolicy> class ScopedUnlocker; namespace detail { /* * A helper alias that resolves to "const T" if the template parameter * is a const Synchronized<T>, or "T" if the parameter is not const. */ SynchronizedDataType; /* * A helper alias that resolves to a ConstLockedPtr if the template parameter * is a const Synchronized<T>, or a LockedPtr if the parameter is not const. */ LockedPtrType; template < typename Synchronized, typename LockFunc, typename TryLockFunc, typename... Args> class SynchronizedLocker { … }; template < typename Synchronized, typename LockFunc, typename TryLockFunc, typename... Args> auto makeSynchronizedLocker( Synchronized& synchronized, LockFunc&& lockFunc, TryLockFunc&& tryLockFunc, Args&&... args) { … } /** * Acquire locks for multiple Synchronized<T> objects, in a deadlock-safe * manner. * * The function uses the "smart and polite" algorithm from this link * http://howardhinnant.github.io/dining_philosophers.html#Polite * * The gist of the algorithm is that it locks a mutex, then tries to lock the * other mutexes in a non-blocking manner. If all the locks succeed, we are * done, if not, we release the locks we have held, yield to allow other * threads to continue and then block on the mutex that we failed to acquire. * * This allows dynamically yielding ownership of all the mutexes but one, so * that other threads can continue doing work and locking the other mutexes. * See the benchmarks in folly/test/SynchronizedBenchmark.cpp for more. */ template <typename... SynchronizedLocker> auto lock(SynchronizedLocker... lockersIn) -> std::tuple<typename SynchronizedLocker::LockedPtr...> { … } template <typename Synchronized, typename... Args> auto wlock(Synchronized& synchronized, Args&&... args) { … } template <typename Synchronized, typename... Args> auto rlock(Synchronized& synchronized, Args&&... args) { … } template <typename Synchronized, typename... Args> auto ulock(Synchronized& synchronized, Args&&... args) { … } template <typename Synchronized, typename... Args> auto lock(Synchronized& synchronized, Args&&... args) { … } } // namespace detail /** * This class temporarily unlocks a LockedPtr in a scoped manner. */ template <class SynchronizedType, class LockPolicy> class ScopedUnlocker { … }; /** * A LockedPtr keeps a Synchronized<T> object locked for the duration of * LockedPtr's existence. * * It provides access the datum's members directly by using operator->() and * operator*(). * * The LockPolicy parameter controls whether or not the lock is acquired in * exclusive or shared mode. */ template <class SynchronizedType, class LockPolicy> class LockedPtr { … }; /** * Helper functions that should be passed to either a lock() or synchronized() * invocation, these return implementation defined structs that will be used * to lock the synchronized instance appropriately. * * lock(wlock(one), rlock(two), wlock(three)); * synchronized([](auto one, two) { ... }, wlock(one), rlock(two)); * * For example in the above rlock() produces an implementation defined read * locking helper instance and wlock() a write locking helper * * Subsequent arguments passed to these locking helpers, after the first, will * be passed by const-ref to the corresponding function on the synchronized * instance. This means that if the function accepts these parameters by * value, they will be copied. Note that it is not necessary that the primary * locking function will be invoked at all (for eg. the implementation might * just invoke the try*Lock() method) * * // Try to acquire the lock for one second * synchronized([](auto) { ... }, wlock(one, 1s)); * * // The timed lock acquire might never actually be called, if it is not * // needed by the underlying deadlock avoiding algorithm * synchronized([](auto, auto) { ... }, rlock(one), wlock(two, 1s)); * * Note that the arguments passed to to *lock() calls will be passed by * const-ref to the function invocation, as the implementation might use them * many times */ template <typename D, typename M, typename... Args> auto wlock(Synchronized<D, M>& synchronized, Args&&... args) { … } template <typename D, typename M, typename... Args> auto wlock(const Synchronized<D, M>& synchronized, Args&&... args) { … } template <typename Data, typename Mutex, typename... Args> auto rlock(const Synchronized<Data, Mutex>& synchronized, Args&&... args) { … } template <typename D, typename M, typename... Args> auto ulock(Synchronized<D, M>& synchronized, Args&&... args) { … } template <typename D, typename M, typename... Args> auto lock(Synchronized<D, M>& synchronized, Args&&... args) { … } template <typename D, typename M, typename... Args> auto lock(const Synchronized<D, M>& synchronized, Args&&... args) { … } /** * Acquire locks for multiple Synchronized<> objects, in a deadlock-safe * manner. * * Wrap the synchronized instances with the appropriate locking strategy by * using one of the four strategies - folly::lock (exclusive acquire for * exclusive only mutexes), folly::rlock (shared acquire for shareable * mutexes), folly::wlock (exclusive acquire for shareable mutexes) or * folly::ulock (upgrade acquire for upgrade mutexes) (see above) * * The locks will be acquired and the passed callable will be invoked with the * LockedPtr instances in the order that they were passed to the function */ template <typename Func, typename... SynchronizedLockers> decltype(auto) synchronized(Func&& func, SynchronizedLockers&&... lockers) { … } /** * Acquire locks on many lockables or synchronized instances in such a way * that the sequence of calls within the function does not cause deadlocks. * * This can often result in a performance boost as compared to simply * acquiring your locks in an ordered manner. Even for very simple cases. * The algorithm tried to adjust to contention by blocking on the mutex it * thinks is the best fit, leaving all other mutexes open to be locked by * other threads. See the benchmarks in folly/test/SynchronizedBenchmark.cpp * for more * * This works differently as compared to the locking algorithm in libstdc++ * and is the recommended way to acquire mutexes in a generic order safe * manner. Performance benchmarks show that this does better than the one in * libstdc++ even for the simple cases * * Usage is the same as std::lock() for arbitrary lockables * * folly::lock(one, two, three); * * To make it work with folly::Synchronized you have to specify how you want * the locks to be acquired, use the folly::wlock(), folly::rlock(), * folly::ulock() and folly::lock() helpers defined below * * auto [one, two] = lock(folly::wlock(a), folly::rlock(b)); * * Note that you can/must avoid the folly:: namespace prefix on the lock() * function if you use the helpers, ADL lookup is done to find the lock function * * This will execute the deadlock avoidance algorithm and acquire a write lock * for a and a read lock for b */ template <typename LockableOne, typename LockableTwo, typename... Lockables> void lock(LockableOne& one, LockableTwo& two, Lockables&... lockables) { … } /** * Acquire locks for multiple Synchronized<T> objects, in a deadlock-safe * manner. * * The locks are acquired in order from lowest address to highest address. * (Note that this is not necessarily the same algorithm used by std::lock().) * For parameters that are const and support shared locks, a read lock is * acquired. Otherwise an exclusive lock is acquired. * * use lock() with folly::wlock(), folly::rlock() and folly::ulock() for * arbitrary locking without causing a deadlock (as much as possible), with the * same effects as std::lock() */ template <class Sync1, class Sync2> std::tuple<detail::LockedPtrType<Sync1>, detail::LockedPtrType<Sync2>> acquireLocked(Sync1& l1, Sync2& l2) { … } /** * A version of acquireLocked() that returns a std::pair rather than a * std::tuple, which is easier to use in many places. */ template <class Sync1, class Sync2> std::pair<detail::LockedPtrType<Sync1>, detail::LockedPtrType<Sync2>> acquireLockedPair(Sync1& l1, Sync2& l2) { … } /************************************************************************ * NOTE: All APIs below this line will be deprecated in upcoming diffs. ************************************************************************/ // Non-member swap primitive template <class T, class M> void swap(Synchronized<T, M>& lhs, Synchronized<T, M>& rhs) { … } /** * Disambiguate the name var by concatenating the line number of the original * point of expansion. This avoids shadowing warnings for nested * SYNCHRONIZEDs. The name is consistent if used multiple times within * another macro. * Only for internal use. */ #define SYNCHRONIZED_VAR(var) … namespace detail { struct [[deprecated( "use explicit lock(), wlock(), or rlock() instead")]] SYNCHRONIZED_macro_is_deprecated { … }; } // namespace detail /** * NOTE: This API is deprecated. Use lock(), wlock(), rlock() or the withLock * functions instead. In the future it will be marked with a deprecation * attribute to emit build-time warnings, and then it will be removed entirely. * * SYNCHRONIZED is the main facility that makes Synchronized<T> * helpful. It is a pseudo-statement that introduces a scope where the * object is locked. Inside that scope you get to access the unadorned * datum. * * Example: * * Synchronized<vector<int>> svector; * ... * SYNCHRONIZED (svector) { ... use svector as a vector<int> ... } * or * SYNCHRONIZED (v, svector) { ... use v as a vector<int> ... } * * Refer to folly/docs/Synchronized.md for a detailed explanation and more * examples. */ #define SYNCHRONIZED(...) … /** * NOTE: This API is deprecated. Use lock(), wlock(), rlock() or the withLock * functions instead. In the future it will be marked with a deprecation * attribute to emit build-time warnings, and then it will be removed entirely. * * Similar to SYNCHRONIZED, but only uses a read lock. */ #define SYNCHRONIZED_CONST(...) … /** * NOTE: This API is deprecated. Use lock(), wlock(), rlock() or the withLock * functions instead. In the future it will be marked with a deprecation * attribute to emit build-time warnings, and then it will be removed entirely. * * Synchronizes two Synchronized objects (they may encapsulate * different data). Synchronization is done in increasing address of * object order, so there is no deadlock risk. */ #define SYNCHRONIZED_DUAL(n1, e1, n2, e2) … } /* namespace folly */