Commit 9bcfbd09 authored by Aaryaman Sagar's avatar Aaryaman Sagar Committed by Facebook Github Bot

Add folly::synchronized

Summary:
Some people said the read-lock-when-const behavior of the `SYNCHRONIZED` macro
was desirable, so instead of codemodding the `SYNCHRONIZED` macros out of
existence in favor of `withRLock`, `withWLock` and `withLock`, add a
generalization of the `SYNCHRONIZED()` macros, so they can be removed in favor
of this, which has identical behavior while being more general.

Reviewed By: yfeldblum

Differential Revision: D8248203

fbshipit-source-id: eb3c1444182e42bea79a377a66606d91a6e517bd
parent cd8d5de0
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
#include <folly/Traits.h> #include <folly/Traits.h>
#include <folly/Utility.h> #include <folly/Utility.h>
#include <folly/container/Foreach.h> #include <folly/container/Foreach.h>
#include <folly/functional/ApplyTuple.h>
#include <glog/logging.h> #include <glog/logging.h>
#include <array> #include <array>
...@@ -827,86 +828,63 @@ using LockedPtrType = typename std::conditional< ...@@ -827,86 +828,63 @@ using LockedPtrType = typename std::conditional<
typename SynchronizedType::ConstLockedPtr, typename SynchronizedType::ConstLockedPtr,
typename SynchronizedType::LockedPtr>::type; typename SynchronizedType::LockedPtr>::type;
template <typename Synchronized> template <
class SynchronizedLockerBase { typename Synchronized,
typename LockFunc,
typename TryLockFunc,
typename... Args>
class SynchronizedLocker {
public: public:
explicit SynchronizedLockerBase(Synchronized& sync) : synchronized{sync} {} using LockedPtr = invoke_result_t<LockFunc&, Synchronized&, const Args&...>;
protected: template <typename LockFuncType, typename TryLockFuncType, typename... As>
Synchronized& synchronized; SynchronizedLocker(
}; Synchronized& sync,
LockFuncType&& lockFunc,
template <typename Synchronized> TryLockFuncType tryLockFunc,
class SynchronizedWLocker : public SynchronizedLockerBase<Synchronized> { As&&... as)
public: : synchronized{sync},
using SynchronizedLockerBase<Synchronized>::SynchronizedLockerBase; lockFunc_{std::forward<LockFuncType>(lockFunc)},
using LockedPtr = typename Synchronized::LockedPtr; tryLockFunc_{std::forward<TryLockFuncType>(tryLockFunc)},
args_{std::forward<As>(as)...} {}
auto lock() {
return this->synchronized.wlock();
}
auto tryLock() {
return this->synchronized.tryWLock();
}
};
template <typename Synchronized>
class SynchronizedRLocker : public SynchronizedLockerBase<Synchronized const> {
public:
using SynchronizedLockerBase<Synchronized const>::SynchronizedLockerBase;
using LockedPtr = typename Synchronized::ConstLockedPtr;
auto lock() { auto lock() const {
return this->synchronized.rlock(); auto args = std::tuple<const Args&...>{args_};
return apply(lockFunc_, std::tuple_cat(std::tie(synchronized), args));
} }
auto tryLock() { auto tryLock() const {
return this->synchronized.tryRLock(); return tryLockFunc_(synchronized);
}
};
template <typename Synchronized>
class SynchronizedULocker : public SynchronizedLockerBase<Synchronized> {
public:
using SynchronizedLockerBase<Synchronized>::SynchronizedLockerBase;
using LockedPtr = typename Synchronized::UpgradeLockedPtr;
auto lock() {
return this->synchronized.ulock();
}
auto tryLock() {
return this->synchronized.tryULock();
}
};
template <typename Synchronized>
class SynchronizedLocker : public SynchronizedLockerBase<Synchronized> {
public:
using SynchronizedLockerBase<Synchronized>::SynchronizedLockerBase;
using LockedPtr = typename Synchronized::LockedPtr;
auto lock() {
return this->synchronized.lock();
}
auto tryLock() {
return this->synchronized.tryLock();
}
};
template <typename Lockable>
class LockableLocker {
public:
explicit LockableLocker(Lockable& lockableIn) : lockable{lockableIn} {}
using LockedPtr = std::unique_lock<Lockable>;
auto lock() {
return std::unique_lock<Lockable>{lockable};
}
auto tryLock() {
auto lock = std::unique_lock<Lockable>{lockable, std::defer_lock};
lock.try_lock();
return lock;
} }
private: private:
Lockable& lockable; Synchronized& synchronized;
LockFunc lockFunc_;
TryLockFunc tryLockFunc_;
std::tuple<Args...> args_;
}; };
template <
typename Synchronized,
typename LockFunc,
typename TryLockFunc,
typename... Args>
auto makeSynchronizedLocker(
Synchronized& synchronized,
LockFunc&& lockFunc,
TryLockFunc&& tryLockFunc,
Args&&... args) {
using LockFuncType = std::decay_t<LockFunc>;
using TryLockFuncType = std::decay_t<TryLockFunc>;
return SynchronizedLocker<
Synchronized,
LockFuncType,
TryLockFuncType,
std::decay_t<Args>...>{synchronized,
std::forward<LockFunc>(lockFunc),
std::forward<TryLockFunc>(tryLockFunc),
std::forward<Args>(args)...};
}
/** /**
* Acquire locks for multiple Synchronized<T> objects, in a deadlock-safe * Acquire locks for multiple Synchronized<T> objects, in a deadlock-safe
* manner. * manner.
...@@ -924,7 +902,8 @@ class LockableLocker { ...@@ -924,7 +902,8 @@ class LockableLocker {
* See the benchmarks in folly/test/SynchronizedBenchmark.cpp for more. * See the benchmarks in folly/test/SynchronizedBenchmark.cpp for more.
*/ */
template <typename... SynchronizedLocker> template <typename... SynchronizedLocker>
auto /* std::tuple<LockedPtr...> */ lock(SynchronizedLocker... lockersIn) { auto lock(SynchronizedLocker... lockersIn)
-> std::tuple<typename SynchronizedLocker::LockedPtr...> {
// capture the list of lockers as a tuple // capture the list of lockers as a tuple
auto lockers = std::forward_as_tuple(lockersIn...); auto lockers = std::forward_as_tuple(lockersIn...);
...@@ -976,6 +955,47 @@ auto /* std::tuple<LockedPtr...> */ lock(SynchronizedLocker... lockersIn) { ...@@ -976,6 +955,47 @@ auto /* std::tuple<LockedPtr...> */ lock(SynchronizedLocker... lockersIn) {
} }
} }
template <typename Synchronized, typename... Args>
auto wlock(Synchronized& synchronized, Args&&... args) {
return detail::makeSynchronizedLocker(
synchronized,
[](auto& s, auto&&... a) {
return s.wlock(std::forward<decltype(a)>(a)...);
},
[](auto& s) { return s.tryWLock(); },
std::forward<Args>(args)...);
}
template <typename Synchronized, typename... Args>
auto rlock(Synchronized& synchronized, Args&&... args) {
return detail::makeSynchronizedLocker(
synchronized,
[](auto& s, auto&&... a) {
return s.rlock(std::forward<decltype(a)>(a)...);
},
[](auto& s) { return s.tryRLock(); },
std::forward<Args>(args)...);
}
template <typename Synchronized, typename... Args>
auto ulock(Synchronized& synchronized, Args&&... args) {
return detail::makeSynchronizedLocker(
synchronized,
[](auto& s, auto&&... a) {
return s.ulock(std::forward<decltype(a)>(a)...);
},
[](auto& s) { return s.tryULock(); },
std::forward<Args>(args)...);
}
template <typename Synchronized, typename... Args>
auto lock(Synchronized& synchronized, Args&&... args) {
return detail::makeSynchronizedLocker(
synchronized,
[](auto& s, auto&&... a) {
return s.lock(std::forward<decltype(a)>(a)...);
},
[](auto& s) { return s.tryLock(); },
std::forward<Args>(args)...);
}
} // namespace detail } // namespace detail
/** /**
...@@ -1494,6 +1514,84 @@ class LockedPtr : public LockedPtrBase< ...@@ -1494,6 +1514,84 @@ class LockedPtr : public LockedPtrBase<
} }
}; };
/**
* 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) {
return detail::wlock(synchronized, std::forward<Args>(args)...);
}
template <typename D, typename M, typename... Args>
auto wlock(const Synchronized<D, M>& synchronized, Args&&... args) {
return detail::wlock(synchronized, std::forward<Args>(args)...);
}
template <typename Data, typename Mutex, typename... Args>
auto rlock(const Synchronized<Data, Mutex>& synchronized, Args&&... args) {
return detail::rlock(synchronized, std::forward<Args>(args)...);
}
template <typename D, typename M, typename... Args>
auto ulock(Synchronized<D, M>& synchronized, Args&&... args) {
return detail::ulock(synchronized, std::forward<Args>(args)...);
}
template <typename D, typename M, typename... Args>
auto ulock(const Synchronized<D, M>& synchronized, Args&&... args) {
return detail::ulock(synchronized, std::forward<Args>(args)...);
}
template <typename D, typename M, typename... Args>
auto lock(Synchronized<D, M>& synchronized, Args&&... args) {
return detail::lock(synchronized, std::forward<Args>(args)...);
}
template <typename D, typename M, typename... Args>
auto lock(const Synchronized<D, M>& synchronized, Args&&... args) {
return detail::lock(synchronized, std::forward<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) {
return apply(
std::forward<Func>(func),
lock(std::forward<SynchronizedLockers>(lockers)...));
}
/** /**
* Acquire locks on many lockables or synchronized instances in such a way * Acquire locks on many lockables or synchronized instances in such a way
* that the sequence of calls within the function does not cause deadlocks. * that the sequence of calls within the function does not cause deadlocks.
...@@ -1528,43 +1626,24 @@ class LockedPtr : public LockedPtrBase< ...@@ -1528,43 +1626,24 @@ class LockedPtr : public LockedPtrBase<
*/ */
template <typename LockableOne, typename LockableTwo, typename... Lockables> template <typename LockableOne, typename LockableTwo, typename... Lockables>
void lock(LockableOne& one, LockableTwo& two, Lockables&... lockables) { void lock(LockableOne& one, LockableTwo& two, Lockables&... lockables) {
auto locks = lock( auto locker = [](auto& lockable) {
detail::LockableLocker<LockableOne>{one}, using Lockable = std::remove_reference_t<decltype(lockable)>;
detail::LockableLocker<LockableTwo>{two}, return detail::makeSynchronizedLocker(
detail::LockableLocker<Lockables>{lockables}...); lockable,
[](auto& l) { return std::unique_lock<Lockable>{l}; },
[](auto& l) {
auto lock = std::unique_lock<Lockable>{l, std::defer_lock};
lock.try_lock();
return lock;
});
};
auto locks = lock(locker(one), locker(two), locker(lockables)...);
// release ownership of the locks from the RAII lock wrapper returned by the // release ownership of the locks from the RAII lock wrapper returned by the
// function above // function above
for_each(locks, [&](auto& lock) { lock.release(); }); for_each(locks, [&](auto& lock) { lock.release(); });
} }
/**
* Helper functions that should be passed to a lock() invocation, these return
* implementation defined structs that lock() will use to lock the
* synchronized instance appropriately.
*
* lock(folly::wlock(one), folly::rlock(two), folly::wlock(three));
*
* For example in the above rlock() produces an implementation defined read
* locking helper instance and wlock() a write locking helper
*/
template <typename Data, typename Mutex>
auto wlock(Synchronized<Data, Mutex>& synchronized) {
return detail::SynchronizedWLocker<Synchronized<Data, Mutex>>{synchronized};
}
template <typename Data, typename Mutex>
auto rlock(Synchronized<Data, Mutex> const& synchronized) {
return detail::SynchronizedRLocker<Synchronized<Data, Mutex>>{synchronized};
}
template <typename Data, typename Mutex>
auto ulock(Synchronized<Data, Mutex>& synchronized) {
return detail::SynchronizedULocker<Synchronized<Data, Mutex>>{synchronized};
}
template <typename Data, typename Mutex>
auto lock(Synchronized<Data, Mutex>& synchronized) {
return detail::SynchronizedLocker<Synchronized<Data, Mutex>>{synchronized};
}
/** /**
* Acquire locks for multiple Synchronized<T> objects, in a deadlock-safe * Acquire locks for multiple Synchronized<T> objects, in a deadlock-safe
* manner. * manner.
......
...@@ -632,6 +632,61 @@ class TryLockable { ...@@ -632,6 +632,61 @@ class TryLockable {
folly::Function<void()> onUnlock; folly::Function<void()> onUnlock;
}; };
struct TestSharedMutex {
public:
void lock() {
onLock_();
}
void unlock() {
onUnlock_();
}
void lock_shared() {
onLockShared_();
}
void unlock_shared() {
onUnlockShared_();
}
bool try_lock() {
onLock_();
return true;
}
bool try_lock_shared() {
onLockShared_();
return true;
}
std::function<void()> onLock_;
std::function<void()> onUnlock_;
std::function<void()> onLockShared_;
std::function<void()> onUnlockShared_;
};
struct TestMutex {
public:
void lock() {
onLock();
++numTimesLocked;
}
bool try_lock() {
if (shouldTryLockSucceed) {
lock();
return true;
}
return false;
}
void unlock() {
onUnlock();
++numTimesUnlocked;
}
int numTimesLocked{0};
int numTimesUnlocked{0};
bool shouldTryLockSucceed{true};
std::function<void()> onLock{[] {}};
std::function<void()> onUnlock{[] {}};
};
template <int kLockable, typename Func> template <int kLockable, typename Func>
void testTryLock(Func func) { void testTryLock(Func func) {
{ {
...@@ -850,42 +905,15 @@ TEST(FollyLockTest, TestVariadicLockWithArbitraryLockables) { ...@@ -850,42 +905,15 @@ TEST(FollyLockTest, TestVariadicLockWithArbitraryLockables) {
EXPECT_TRUE(lckTwo); EXPECT_TRUE(lckTwo);
} }
namespace {
struct TestLock {
public:
void lock() {
onLock();
++numTimesLocked;
}
bool try_lock() {
if (shouldTryLockSucceed) {
lock();
return true;
}
return false;
}
void unlock() {
onUnlock();
++numTimesUnlocked;
}
int numTimesLocked{0};
int numTimesUnlocked{0};
bool shouldTryLockSucceed{true};
std::function<void()> onLock{[] {}};
std::function<void()> onUnlock{[] {}};
};
} // namespace
TEST(FollyLockTest, TestVariadicLockSmartAndPoliteAlgorithm) { TEST(FollyLockTest, TestVariadicLockSmartAndPoliteAlgorithm) {
auto one = TestLock{}; auto one = TestMutex{};
auto two = TestLock{}; auto two = TestMutex{};
auto three = TestLock{}; auto three = TestMutex{};
auto makeReset = [&] { auto makeReset = [&] {
return folly::makeGuard([&] { return folly::makeGuard([&] {
one = TestLock{}; one = TestMutex{};
two = TestLock{}; two = TestMutex{};
three = TestLock{}; three = TestMutex{};
}); });
}; };
...@@ -945,4 +973,58 @@ TEST(FollyLockTest, TestVariadicLockSmartAndPoliteAlgorithm) { ...@@ -945,4 +973,58 @@ TEST(FollyLockTest, TestVariadicLockSmartAndPoliteAlgorithm) {
} }
} }
TEST(SynchronizedAlgorithmTest, Basic) {
auto sync = Synchronized<int>{0};
auto value = synchronized([](auto s) { return *s; }, wlock(sync));
EXPECT_EQ(value, 0);
}
TEST(SynchronizedAlgorithmTest, BasicNonShareableMutex) {
auto sync = Synchronized<int, std::mutex>{0};
auto value = synchronized([](auto s) { return *s; }, lock(sync));
EXPECT_EQ(value, 0);
}
TEST(Synchronized, SynchronizedFunctionNonConst) {
auto locked = 0;
auto unlocked = 0;
auto sync = Synchronized<int, TestSharedMutex>{
std::piecewise_construct,
std::make_tuple(0),
std::make_tuple([&] { ++locked; }, [&] { ++unlocked; }, [] {}, [] {})};
synchronized([](auto) {}, wlock(sync));
EXPECT_EQ(locked, 1);
EXPECT_EQ(unlocked, 1);
}
TEST(Synchronized, SynchronizedFunctionConst) {
auto locked = 0;
auto unlocked = 0;
auto sync = Synchronized<int, TestSharedMutex>{
std::piecewise_construct,
std::make_tuple(0),
std::make_tuple([] {}, [] {}, [&] { ++locked; }, [&] { ++unlocked; })};
synchronized([](auto) {}, rlock(sync));
EXPECT_EQ(locked, 1);
EXPECT_EQ(unlocked, 1);
}
TEST(Synchronized, SynchronizedFunctionManyObjects) {
auto fail = [] { EXPECT_TRUE(false); };
auto pass = [] {};
auto one = Synchronized<int, TestSharedMutex>{
std::piecewise_construct,
std::make_tuple(0),
std::make_tuple(pass, pass, fail, fail)};
auto two = Synchronized<std::string, TestSharedMutex>{
std::piecewise_construct,
std::make_tuple(),
std::make_tuple(fail, fail, pass, pass)};
synchronized([](auto, auto) {}, wlock(one), rlock(two));
}
} // namespace folly } // namespace folly
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment