Skip to content

Instantly share code, notes, and snippets.

@Seneral
Last active November 11, 2023 17:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Seneral/4cee137118ff262d95424aa3c5599439 to your computer and use it in GitHub Desktop.
Save Seneral/4cee137118ff262d95424aa3c5599439 to your computer and use it in GitHub Desktop.
Standalone Folly Synchronized - With std::shared_mutex (no upgrade) - see revision for changes from original
/*
* 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.
*/
#pragma once
#include <chrono>
#include <mutex>
#include <shared_mutex>
#include <system_error>
#include <type_traits>
/**
* NOTE: Dropped unsafe_for_async_usage_if (tagging structs with folly_is_unsafe_for_async_usage)
* That custom tagging system was used for their SharedMutex implementation
* Since it is not used in this standalone version, it is dropped
*/
template<typename ...Ts> struct make_void
{
using type = void;
};
template<typename ...Ts> using void_t = typename make_void<Ts...>::type;
// https://en.cppreference.com/w/cpp/experimental/is_detected
namespace folly
{
namespace detail
{
struct nonesuch
{
nonesuch() = delete;
~nonesuch() = delete;
nonesuch(nonesuch const&) = delete;
nonesuch(nonesuch const&&) = delete;
void operator=(nonesuch const&) = delete;
void operator=(nonesuch&&) = delete;
};
template<class Default,
class AlwaysVoid,
template<class...> class Op,
class... Args>
struct detector
{
using value_t = std::false_type;
using type = Default;
};
template<class Default, template<class...> class Op, class... Args>
struct detector<Default, void_t<Op<Args...>>, Op, Args...>
{
using value_t = std::true_type;
using type = Op<Args...>;
};
template<template<class...> class Op, class... Args>
using is_detected = typename detector<nonesuch, void, Op, Args...>::value_t;
template<template<class...> class Op, class... Args>
using detected_t = typename detector<nonesuch, void, Op, Args...>::type;
template<class Default, template<class...> class Op, class... Args>
using detected_or = detector<Default, void, Op, Args...>;
template<class Default, template<class...> class Op, class... Args>
using detected_or_t = typename detected_or<Default, Op, Args...>::type;
template<class Expected, template<class...> class Op, class... Args>
using is_detected_exact = std::is_same<Expected, detected_t<Op, Args...>>;
template<class To, template<class...> class Op, class... Args>
using is_detected_convertible =
std::is_convertible<detected_t<Op, Args...>, To>;
} // namespace detail
} // namespace folly
namespace folly {
namespace access {
#define FOLLY_CREATE_MEMBER_INVOKER_SUITE(membername) \
struct membername##_fn { \
template <typename O, typename... Args> \
inline constexpr auto operator()( \
O&& o, Args&&... args) const \
noexcept(noexcept( \
static_cast<O&&>(o).membername(static_cast<Args&&>(args)...))) \
-> decltype(static_cast<O&&>(o).membername( \
static_cast<Args&&>(args)...)) { \
return static_cast<O&&>(o).membername(static_cast<Args&&>(args)...); \
} \
}; \
inline constexpr membername##_fn membername {}
// locks and unlocks
FOLLY_CREATE_MEMBER_INVOKER_SUITE(lock);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock_for);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock_until);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(unlock);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(lock_shared);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock_shared);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock_shared_for);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock_shared_until);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(unlock_shared);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(lock_upgrade);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock_upgrade);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock_upgrade_for);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_lock_upgrade_until);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(unlock_upgrade);
// transitions
FOLLY_CREATE_MEMBER_INVOKER_SUITE(unlock_and_lock_shared);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(unlock_and_lock_upgrade);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_shared_and_lock);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_shared_and_lock_for);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_shared_and_lock_until);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_shared_and_lock_upgrade);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_shared_and_lock_upgrade_for);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_shared_and_lock_upgrade_until);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(unlock_upgrade_and_lock);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_upgrade_and_lock);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_upgrade_and_lock_for);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(try_unlock_upgrade_and_lock_until);
FOLLY_CREATE_MEMBER_INVOKER_SUITE(unlock_upgrade_and_lock_shared);
} // namespace access
namespace detail {
// Ordinarily, there would not be any need for this lock_storage class and the
// lock_base class can just have the entire implementation with a little bit
// of sfinae to handle with-state v.s. sans-state.
//
// Unfortunately, vc2017 fails to resolve calls to the adopt_lock ctor of such
// a lock_base class implemented with sfinae. The only observed workaround is
// to extract the adopt_lock ctor to a pair of dependent base classes, plus the
// minimum necessary to make it all work. In particular, none of the locking or
// unlocking functions are needed here, which makes lock_storage actually quite
// minimal.
//
// Unfortunately, this workaround leaves lock_base marvelously odd with extra
// syntax noise everywhere: c'est la vie.
template <typename Mutex, typename LockState>
struct lock_storage {
Mutex* mutex_{};
LockState state_{};
lock_storage() = default;
lock_storage(lock_storage&& that) noexcept
: mutex_{std::exchange(that.mutex_, nullptr)},
state_{std::exchange(that.state_, LockState{})} {}
lock_storage(Mutex& mutex, std::adopt_lock_t, LockState const& state)
: mutex_{std::addressof(mutex)}, state_{state} {
state_ || (check_fail_(), 0);
}
void operator=(lock_storage&&) = delete;
private:
[[noreturn]] void check_fail_() {
auto code = std::errc::operation_not_permitted;
throw std::system_error(std::make_error_code(code));
}
};
template <typename Mutex>
struct lock_storage<Mutex, void> {
Mutex* mutex_{};
bool state_{};
lock_storage() = default;
lock_storage(lock_storage&& that) noexcept
: mutex_{std::exchange(that.mutex_, nullptr)},
state_{std::exchange(that.state_, false)} {}
lock_storage(Mutex& mutex, std::adopt_lock_t)
: mutex_{std::addressof(mutex)}, state_{true} {}
void operator=(lock_storage&&) = delete;
};
// A lock base class with a mostly-complete implementation suitable for either
// unique, shared, or upgrade lock base classes. However, each particular base
// class specific to each lock category must still be its own class to avoid
// overly permissive overloads of member and free swap.
template <typename Mutex, typename Policy>
class lock_base //
: private lock_storage<
Mutex,
std::invoke_result_t<typename Policy::lock_fn, Mutex&>> {
public:
using mutex_type = Mutex;
using state_type = std::invoke_result_t<typename Policy::lock_fn, mutex_type&>;
static_assert(
std::is_same<state_type, std::decay_t<state_type>>::value,
"state_type, if not void, must be a value type");
static_assert(
std::is_void<state_type>::value ||
(std::is_nothrow_default_constructible<state_type>::value &&
std::is_nothrow_copy_constructible<state_type>::value &&
std::is_nothrow_copy_assignable<state_type>::value &&
std::is_nothrow_destructible<state_type>::value),
"state_type, if not void, must be noexcept-semiregular");
static_assert(
std::is_void<state_type>::value ||
std::is_constructible<bool, state_type>::value,
"state_type, if not void, must explicitly convert to bool");
private:
using storage = lock_storage<mutex_type, state_type>;
static constexpr bool has_state_ = !std::is_void<state_type>::value;
template <bool C>
using if_ = std::enable_if_t<C, int>;
public:
using storage::storage;
lock_base() = default;
lock_base(lock_base&&) = default;
explicit lock_base(mutex_type& mutex) {
storage::mutex_ = std::addressof(mutex);
lock();
}
lock_base(mutex_type& mutex, std::defer_lock_t) noexcept {
storage::mutex_ = std::addressof(mutex);
}
lock_base(mutex_type& mutex, std::try_to_lock_t) {
storage::mutex_ = std::addressof(mutex);
try_lock();
}
template <typename Rep, typename Period>
lock_base(
mutex_type& mutex, std::chrono::duration<Rep, Period> const& timeout) {
storage::mutex_ = std::addressof(mutex);
try_lock_for(timeout);
}
template <typename Clock, typename Duration>
lock_base(
mutex_type& mutex,
std::chrono::time_point<Clock, Duration> const& deadline) {
storage::mutex_ = std::addressof(mutex);
try_lock_until(deadline);
}
~lock_base() {
if (owns_lock()) {
unlock();
}
}
lock_base& operator=(lock_base&& that) noexcept {
if (owns_lock()) {
unlock();
}
storage::mutex_ = std::exchange(that.mutex_, nullptr);
storage::state_ = std::exchange(that.state_, decltype(storage::state_){});
return *this;
}
template <bool C = has_state_, if_<!C> = 0>
void lock() {
check<false>();
typename Policy::lock_fn{}(*storage::mutex_);
storage::state_ = true;
}
template <bool C = has_state_, if_<C> = 0>
void lock() {
check<false>();
storage::state_ = typename Policy::lock_fn{}(*storage::mutex_);
}
bool try_lock() {
check<false>();
storage::state_ = typename Policy::try_lock_fn{}(*storage::mutex_);
return !!storage::state_;
}
template <typename Rep, typename Period>
bool try_lock_for(std::chrono::duration<Rep, Period> const& timeout) {
check<false>();
storage::state_ =
typename Policy::try_lock_for_fn{}(*storage::mutex_, timeout);
return !!storage::state_;
}
template <typename Clock, typename Duration>
bool try_lock_until(
std::chrono::time_point<Clock, Duration> const& deadline) {
check<false>();
storage::state_ =
typename Policy::try_lock_until_fn{}(*storage::mutex_, deadline);
return !!storage::state_;
}
template <bool C = has_state_, if_<!C> = 0>
void unlock() {
check<true>();
typename Policy::unlock_fn{}(*storage::mutex_);
storage::state_ = decltype(storage::state_){};
}
template <bool C = has_state_, if_<C> = 0>
void unlock() {
check<true>();
auto const& state = storage::state_; // prevent unlock from mutating state_
typename Policy::unlock_fn{}(*storage::mutex_, state);
storage::state_ = decltype(storage::state_){};
}
mutex_type* release() noexcept {
storage::state_ = {};
return std::exchange(storage::mutex_, nullptr);
}
mutex_type* mutex() const noexcept { return storage::mutex_; }
template <bool C = has_state_, if_<C> = 0>
state_type state() const noexcept {
return storage::state_;
}
bool owns_lock() const noexcept { return !!storage::state_; }
explicit operator bool() const noexcept { return !!storage::state_; }
protected:
void swap(lock_base& that) noexcept {
std::swap(storage::mutex_, that.mutex_);
std::swap(storage::state_, that.state_);
}
private:
template <bool Owns>
void check() {
if (!storage::mutex_ || !storage::state_ == Owns) {
check_fail_<Owns>();
}
}
template <bool Owns>
[[noreturn]] void check_fail_() {
auto perm = std::errc::operation_not_permitted;
auto dead = std::errc::resource_deadlock_would_occur;
auto code = !storage::mutex_ || !storage::state_ ? perm : dead;
throw std::system_error(std::make_error_code(code));
}
};
template <typename Mutex, typename Policy>
class lock_guard_base {
private:
using lock_type_ = lock_base<Mutex, Policy>;
using lock_state_type_ = typename lock_type_::state_type;
static constexpr bool has_state_ = !std::is_void<lock_state_type_>::value;
using state_type_ = std::conditional_t<has_state_, lock_state_type_, bool>;
template <bool C>
using if_ = std::enable_if_t<C, int>;
public:
using mutex_type = Mutex;
lock_guard_base(lock_guard_base const&) = delete;
lock_guard_base(lock_guard_base&&) = delete;
explicit lock_guard_base(mutex_type& mutex) : lock_{mutex} {}
template <bool C = has_state_, if_<!C> = 0>
lock_guard_base(mutex_type& mutex, std::adopt_lock_t)
: lock_{mutex, std::adopt_lock} {}
template <bool C = has_state_, if_<C> = 0>
lock_guard_base(
mutex_type& mutex, std::adopt_lock_t, state_type_ const& state)
: lock_{mutex, std::adopt_lock, state} {}
void operator=(lock_guard_base const&) = delete;
void operator=(lock_guard_base&&) = delete;
private:
lock_type_ lock_;
};
struct lock_policy_unique {
using lock_fn = access::lock_fn;
using try_lock_fn = access::try_lock_fn;
using try_lock_for_fn = access::try_lock_for_fn;
using try_lock_until_fn = access::try_lock_until_fn;
using unlock_fn = access::unlock_fn;
};
struct lock_policy_shared {
using lock_fn = access::lock_shared_fn;
using try_lock_fn = access::try_lock_shared_fn;
using try_lock_for_fn = access::try_lock_shared_for_fn;
using try_lock_until_fn = access::try_lock_shared_until_fn;
using unlock_fn = access::unlock_shared_fn;
};
struct lock_policy_upgrade {
using lock_fn = access::lock_upgrade_fn;
using try_lock_fn = access::try_lock_upgrade_fn;
using try_lock_for_fn = access::try_lock_upgrade_for_fn;
using try_lock_until_fn = access::try_lock_upgrade_until_fn;
using unlock_fn = access::unlock_upgrade_fn;
};
template <typename Mutex>
using lock_policy_hybrid = std::conditional_t<
std::is_invocable_v<access::lock_shared_fn, Mutex&>,
lock_policy_shared,
lock_policy_unique>;
template <typename Mutex>
using lock_base_unique = lock_base<Mutex, lock_policy_unique>;
template <typename Mutex>
using lock_base_shared = lock_base<Mutex, lock_policy_shared>;
template <typename Mutex>
using lock_base_upgrade = lock_base<Mutex, lock_policy_upgrade>;
template <typename Mutex>
using lock_base_hybrid = lock_base<Mutex, lock_policy_hybrid<Mutex>>;
} // namespace detail
// unique_lock_base
//
// A lock-holder base which holds exclusive locks, usable with any mutex type.
//
// Works with both lockable mutex types and lockable-with-state mutex types.
//
// When defining lockable-with-state mutex types, specialize std::unique_lock
// to derive this. See the example with upgrade_lock.
//
// A lockable-with-state mutex type is signalled by the return type of mutex
// member function lock. Members try_lock, try_lock_for, and try_lock_until
// all return this type and member unlock accepts this type.
template <typename Mutex>
class unique_lock_base : public detail::lock_base_unique<Mutex> {
private:
using base = detail::lock_base_unique<Mutex>;
using self = unique_lock_base;
public:
using base::base;
void swap(self& that) noexcept { base::swap(that); }
friend void swap(self& a, self& b) noexcept { a.swap(b); }
};
// shared_lock_base
//
// A lock-holder base which holds shared locks, usable with any shared mutex
// type.
//
// Works with both shared-lockable mutex types and shared-lockable-with-state
// mutex types.
//
// When defining shared-lockable-with-state mutex types, specialize
// std::shared_lock to derive this. See the example with upgrade_lock.
//
// A shared-lockable-with-state mutex type is signalled by the return type of
// mutex member function lock_shared. Members try_lock_shared,
// try_lock_shared_for, and try_lock_shared_until all return this type and
// member unlock_shared accepts this type. Likewise for mutex member
// transition functions.
template <typename Mutex>
class shared_lock_base : public detail::lock_base_shared<Mutex> {
private:
using base = detail::lock_base_shared<Mutex>;
using self = shared_lock_base;
public:
using base::base;
void swap(self& that) noexcept { base::swap(that); }
friend void swap(self& a, self& b) noexcept { a.swap(b); }
};
// upgrade_lock_base
//
// A lock-holder base which holds upgrade locks, usable with any upgrade mutex
// type.
//
// Works with both upgrade-lockable mutex types and upgrade-lockable-with-state
// mutex types.
//
// There are no use-cases except the one below.
//
// An upgrade-lockable-with-state mutex type is signalled by the return type of
// mutex member function lock_upgrade. Members try_lock_upgrade,
// try_lock_upgrade_for, and try_lock_upgrade_until all return this type and
// member unlock_upgrade accepts this type. Likewise for mutex member
// transition functions.
template <typename Mutex>
class upgrade_lock_base : public detail::lock_base_upgrade<Mutex> {
private:
using base = detail::lock_base_upgrade<Mutex>;
using self = upgrade_lock_base;
public:
using base::base;
void swap(self& that) noexcept { base::swap(that); }
friend void swap(self& a, self& b) noexcept { a.swap(b); }
};
// hybrid_lock_base
//
// A lock-holder base which holds shared locks for shared mutex types or
// exclusive locks otherwise.
//
// See unique_lock_base and shared_lock_base.
template <typename Mutex>
class hybrid_lock_base : public detail::lock_base_hybrid<Mutex> {
private:
using base = detail::lock_base_hybrid<Mutex>;
using self = hybrid_lock_base;
public:
using base::base;
void swap(self& that) noexcept { base::swap(that); }
friend void swap(self& a, self& b) noexcept { a.swap(b); }
};
// unique_lock
//
// Alias to std::unique_lock.
using std::unique_lock;
// shared_lock
//
// Alias to std::shared_lock.
using std::shared_lock;
// upgrade_lock
//
// A lock-holder type which holds upgrade locks, usable with any upgrade mutex
// type. An upgrade mutex is a shared mutex which supports the upgrade state.
//
// Works with both upgrade-lockable mutex types and upgrade-lockable-with-state
// mutex types.
//
// Upgrade locks are not useful by themselves; they are primarily useful since
// upgrade locks may be transitioned atomically to exclusive locks. This lock-
// holder type works with the transition_to_... functions below to facilitate
// atomic transition from ugprade lock to exclusive lock.
template <typename Mutex>
class upgrade_lock : public upgrade_lock_base<Mutex> {
public:
using upgrade_lock_base<Mutex>::upgrade_lock_base;
};
// hybrid_lock
//
// A lock-holder type which holds shared locks for shared mutex types or
// exclusive locks otherwise.
//
// See unique_lock and shared_lock.
template <typename Mutex>
class hybrid_lock : public hybrid_lock_base<Mutex> {
public:
using hybrid_lock_base<Mutex>::hybrid_lock_base;
};
#if __cpp_deduction_guides >= 201611
template <typename Mutex, typename... A>
explicit hybrid_lock(Mutex&, A const&...) -> hybrid_lock<Mutex>;
#endif
// lock_guard_base
//
// A lock-guard which holds exclusive locks, usable with any mutex type.
//
// Works with both lockable mutex types and lockable-with-state mutex types.
//
// When defining lockable-with-state mutex types, specialize std::lock_guard
// to derive this.
template <typename Mutex>
class unique_lock_guard_base
: public detail::lock_guard_base<Mutex, detail::lock_policy_unique> {
private:
using base = detail::lock_guard_base<Mutex, detail::lock_policy_unique>;
public:
using base::base;
};
// unique_lock_guard
//
// Alias to std::lock_guard.
template <typename Mutex>
using unique_lock_guard = std::lock_guard<Mutex>;
// shared_lock_guard
//
// A lock-guard which holds shared locks, usable with any shared mutex type.
//
// Works with both lockable mutex types and lockable-with-state mutex types.
template <typename Mutex>
class shared_lock_guard
: public detail::lock_guard_base<Mutex, detail::lock_policy_shared> {
private:
using base = detail::lock_guard_base<Mutex, detail::lock_policy_shared>;
public:
using base::base;
};
// hybrid_lock_guard
//
// For shared mutex types, effectively shared_lock_guard; otherwise,
// effectively unique_lock_guard.
template <typename Mutex>
class hybrid_lock_guard
: public detail::lock_guard_base<Mutex, detail::lock_policy_hybrid<Mutex>> {
private:
using base =
detail::lock_guard_base<Mutex, detail::lock_policy_hybrid<Mutex>>;
public:
using base::base;
};
#if __cpp_deduction_guides >= 201611
template <typename Mutex, typename... A>
explicit hybrid_lock_guard(Mutex&, A const&...) -> hybrid_lock_guard<Mutex>;
#endif
// make_unique_lock
//
// Returns a unique_lock constructed with the given arguments. Deduces the
// mutex type.
struct make_unique_lock_fn {
template <typename Mutex, typename... A>
unique_lock<Mutex> operator()(Mutex& mutex, A&&... a) const {
return unique_lock<Mutex>{mutex, static_cast<A&&>(a)...};
}
};
inline constexpr make_unique_lock_fn make_unique_lock{};
// make_shared_lock
//
// Returns a shared_lock constructed with the given arguments. Deduces the
// mutex type.
struct make_shared_lock_fn {
template <typename Mutex, typename... A>
shared_lock<Mutex> operator()(Mutex& mutex, A&&... a) const {
return shared_lock<Mutex>{mutex, static_cast<A&&>(a)...};
}
};
inline constexpr make_shared_lock_fn make_shared_lock{};
// make_upgrade_lock
//
// Returns an upgrade_lock constructed with the given arguments. Deduces the
// mutex type.
struct make_upgrade_lock_fn {
template <typename Mutex, typename... A>
upgrade_lock<Mutex> operator()(Mutex& mutex, A&&... a) const {
return upgrade_lock<Mutex>{mutex, static_cast<A&&>(a)...};
}
};
inline constexpr make_upgrade_lock_fn make_upgrade_lock{};
// make_hybrid_lock
//
// Returns a hybrid_lock constructed with the given arguments. Deduces the
// mutex type.
struct make_hybrid_lock_fn {
template <typename Mutex, typename... A>
hybrid_lock<Mutex> operator()(Mutex& mutex, A&&... a) const {
return hybrid_lock<Mutex>{mutex, static_cast<A&&>(a)...};
}
};
inline constexpr make_hybrid_lock_fn make_hybrid_lock{};
namespace detail {
template <typename L>
using lock_state_type_of_t_ = typename L::state_type;
template <typename L>
using lock_state_type_of_t = detected_or_t<void, lock_state_type_of_t_, L>;
template <typename State>
struct transition_lock_result_ {
template <typename Transition, typename Mutex, typename... A>
using apply = std::invoke_result_t<Transition, Mutex&, State const&, A const&...>;
};
template <>
struct transition_lock_result_<void> {
template <typename Transition, typename Mutex, typename... A>
using apply = std::invoke_result_t<Transition, Mutex&, A const&...>;
};
template <typename From, typename Transition, typename... A>
using transition_lock_result_t_ =
typename transition_lock_result_<lock_state_type_of_t<From>>::
template apply<Transition, typename From::mutex_type&, A...>;
template <
typename From,
typename Transition,
typename... A,
typename FromState = lock_state_type_of_t<From>,
std::enable_if_t<std::is_void<FromState>::value, int> = 0>
auto transition_lock_2_(From& lock, Transition transition, A const&... a) {
// release() may check or mutate mutex state to support the dissociation, call
// it before performing the transition.
return transition(*lock.release(), a...);
}
template <
typename From,
typename Transition,
typename... A,
typename FromState = lock_state_type_of_t<From>,
std::enable_if_t<!std::is_void<FromState>::value, int> = 0>
auto transition_lock_2_(From& lock, Transition transition, A const&... a) {
auto state = lock.state();
// release() may check or mutate mutex state to support the dissociation, call
// it before performing the transition.
return transition(*lock.release(), std::move(state), a...);
}
template <
typename From,
typename Transition,
typename... A,
typename Result = transition_lock_result_t_<From, Transition, A...>,
std::enable_if_t<std::is_void<Result>::value, int> = 0>
auto transition_lock_1_(From& lock, Transition transition, A const&... a) {
return detail::transition_lock_2_(lock, transition, a...), true;
}
template <
typename From,
typename Transition,
typename... A,
typename Result = transition_lock_result_t_<From, Transition, A...>,
std::enable_if_t<!std::is_void<Result>::value, int> = 0>
auto transition_lock_1_(From& lock, Transition transition, A const&... a) {
return detail::transition_lock_2_(lock, transition, a...);
}
template <
typename To,
typename From,
typename Transition,
typename... A,
typename ToState = lock_state_type_of_t<To>,
std::enable_if_t<std::is_void<ToState>::value, int> = 0>
auto transition_lock_0_(From& lock, Transition transition, A const&... a) {
auto& mutex = *lock.mutex();
auto s = detail::transition_lock_1_(lock, transition, a...);
return !s ? To{} : To{mutex, std::adopt_lock};
}
template <
typename To,
typename From,
typename Transition,
typename... A,
typename ToState = lock_state_type_of_t<To>,
std::enable_if_t<!std::is_void<ToState>::value, int> = 0>
auto transition_lock_0_(From& lock, Transition transition, A const&... a) {
auto& mutex = *lock.mutex();
auto s = detail::transition_lock_1_(lock, transition, a...);
return !s ? To{} : To{mutex, std::adopt_lock, s};
}
template <
template <typename>
class To,
template <typename>
class From,
typename Mutex,
typename Transition,
typename... A>
auto transition_lock_(From<Mutex>& lock, Transition transition, A const&... a) {
// clang-format off
return
!lock.mutex() ? To<Mutex>{} :
!lock.owns_lock() ? To<Mutex>{*lock.release(), std::defer_lock} :
detail::transition_lock_0_<To<Mutex>>(lock, transition, a...);
// clang-format on
}
template <typename, typename>
struct transition_lock_policy;
template <typename Mutex>
struct transition_lock_policy<unique_lock<Mutex>, shared_lock<Mutex>> {
using transition_fn = access::unlock_and_lock_shared_fn;
};
template <typename Mutex>
struct transition_lock_policy<unique_lock<Mutex>, upgrade_lock<Mutex>> {
using transition_fn = access::unlock_and_lock_upgrade_fn;
};
template <typename Mutex>
struct transition_lock_policy<shared_lock<Mutex>, unique_lock<Mutex>> {
using try_transition_fn = access::try_unlock_shared_and_lock_fn;
using try_transition_for_fn = access::try_unlock_shared_and_lock_for_fn;
using try_transition_until_fn = access::try_unlock_shared_and_lock_until_fn;
};
template <typename Mutex>
struct transition_lock_policy<shared_lock<Mutex>, upgrade_lock<Mutex>> {
using try_transition_fn = access::try_unlock_shared_and_lock_upgrade_fn;
using try_transition_for_fn =
access::try_unlock_shared_and_lock_upgrade_for_fn;
using try_transition_until_fn =
access::try_unlock_shared_and_lock_upgrade_until_fn;
};
template <typename Mutex>
struct transition_lock_policy<upgrade_lock<Mutex>, unique_lock<Mutex>> {
using transition_fn = access::unlock_upgrade_and_lock_fn;
using try_transition_fn = access::try_unlock_upgrade_and_lock_fn;
using try_transition_for_fn = access::try_unlock_upgrade_and_lock_for_fn;
using try_transition_until_fn = access::try_unlock_upgrade_and_lock_until_fn;
};
template <typename Mutex>
struct transition_lock_policy<upgrade_lock<Mutex>, shared_lock<Mutex>> {
using transition_fn = access::unlock_upgrade_and_lock_shared_fn;
};
} // namespace detail
// transition_lock
//
// Represents an atomic transition from the from-lock to the to-lock. Waits
// unboundedly for the transition to become available.
template <
template <typename>
class ToLock,
typename Mutex,
template <typename>
class FromLock>
ToLock<Mutex> transition_lock(FromLock<Mutex>& lock) {
using policy = detail::transition_lock_policy<FromLock<Mutex>, ToLock<Mutex>>;
auto _ = typename policy::transition_fn{};
return detail::transition_lock_<ToLock>(lock, _);
}
// try_transition_lock
//
// Represents an atomic transition attempt from the from-lock to the to-lock.
// Does not wait if the transition is not immediately available.
template <
template <typename>
class ToLock,
typename Mutex,
template <typename>
class FromLock>
ToLock<Mutex> try_transition_lock(FromLock<Mutex>& lock) {
using policy = detail::transition_lock_policy<FromLock<Mutex>, ToLock<Mutex>>;
auto _ = typename policy::try_transition_fn{};
return detail::transition_lock_<ToLock>(lock, _);
}
// try_transition_lock_for
//
// Represents an atomic transition attempt from the from-lock to the to-lock
// bounded by a timeout. Waits up to the timeout for the transition to become
// available.
template <
template <typename>
class ToLock,
typename Mutex,
template <typename>
class FromLock,
typename Rep,
typename Period>
ToLock<Mutex> try_transition_lock_for(
FromLock<Mutex>& lock, std::chrono::duration<Rep, Period> const& timeout) {
using policy = detail::transition_lock_policy<FromLock<Mutex>, ToLock<Mutex>>;
auto _ = typename policy::try_transition_for_fn{};
return detail::transition_lock_<ToLock>(lock, _, timeout);
}
// try_transition_lock_until
//
// Represents an atomic transition attempt from the from-lock to the to-lock
// bounded by a deadline. Waits up to the deadline for the transition to become
// available.
template <
template <typename>
class ToLock,
typename Mutex,
template <typename>
class FromLock,
typename Clock,
typename Duration>
ToLock<Mutex> try_transition_lock_until(
FromLock<Mutex>& lock,
std::chrono::time_point<Clock, Duration> const& deadline) {
using policy = detail::transition_lock_policy<FromLock<Mutex>, ToLock<Mutex>>;
auto _ = typename policy::try_transition_until_fn{};
return detail::transition_lock_<ToLock>(lock, _, deadline);
}
// transition_to_shared_lock(unique_lock)
//
// Wraps mutex member function unlock_and_lock_shared.
//
// Represents an immediate atomic downgrade transition from exclusive lock to
// to shared lock.
template <typename Mutex>
shared_lock<Mutex> transition_to_shared_lock(unique_lock<Mutex>& lock) {
return transition_lock<shared_lock>(lock);
}
// transition_to_shared_lock(upgrade_lock)
//
// Wraps mutex member function unlock_upgrade_and_lock_shared.
//
// Represents an immediate atomic downgrade transition from upgrade lock to
// shared lock.
template <typename Mutex>
shared_lock<Mutex> transition_to_shared_lock(upgrade_lock<Mutex>& lock) {
return transition_lock<shared_lock>(lock);
}
// transition_to_upgrade_lock(unique_lock)
//
// Wraps mutex member function unlock_and_lock_upgrade.
//
// Represents an immediate atomic downgrade transition from unique lock to
// upgrade lock.
template <typename Mutex>
upgrade_lock<Mutex> transition_to_upgrade_lock(unique_lock<Mutex>& lock) {
return transition_lock<upgrade_lock>(lock);
}
// transition_to_unique_lock(upgrade_lock)
//
// Wraps mutex member function unlock_upgrade_and_lock.
//
// Represents an eventual atomic upgrade transition from upgrade lock to unique
// lock.
template <typename Mutex>
unique_lock<Mutex> transition_to_unique_lock(upgrade_lock<Mutex>& lock) {
return transition_lock<unique_lock>(lock);
}
// try_transition_to_unique_lock(upgrade_lock)
//
// Wraps mutex member function try_unlock_upgrade_and_lock.
//
// Represents an immediate attempted atomic upgrade transition from upgrade
// lock to unique lock.
template <typename Mutex>
unique_lock<Mutex> try_transition_to_unique_lock(upgrade_lock<Mutex>& lock) {
return transition_lock<unique_lock>(lock);
}
// try_transition_to_unique_lock_for(upgrade_lock)
//
// Wraps mutex member function try_unlock_upgrade_and_lock_for.
//
// Represents an eventual attempted atomic upgrade transition from upgrade
// lock to unique lock.
template <typename Mutex, typename Rep, typename Period>
unique_lock<Mutex> try_transition_to_unique_lock_for(
upgrade_lock<Mutex>& lock,
std::chrono::duration<Rep, Period> const& timeout) {
return try_transition_lock_for<unique_lock>(lock, timeout);
}
// try_transition_to_unique_lock_until(upgrade_lock)
//
// Wraps mutex member function try_unlock_upgrade_and_lock_until.
//
// Represents an eventual attempted atomic upgrade transition from upgrade
// lock to unique lock.
template <typename Mutex, typename Clock, typename Duration>
unique_lock<Mutex> try_transition_to_unique_lock_until(
upgrade_lock<Mutex>& lock,
std::chrono::time_point<Clock, Duration> const& deadline) {
return try_transition_lock_until<unique_lock>(lock, deadline);
}
// try_transition_to_unique_lock(shared_lock)
//
// Wraps mutex member function try_unlock_shared_and_lock.
//
// Represents an immediate attempted atomic upgrade transition from shared
// lock to unique lock.
template <typename Mutex>
unique_lock<Mutex> try_transition_to_unique_lock(shared_lock<Mutex>& lock) {
return try_transition_lock<unique_lock>(lock);
}
// try_transition_to_unique_lock_for(shared_lock)
//
// Wraps mutex member function try_unlock_shared_and_lock_for.
//
// Represents an eventual attempted atomic upgrade transition from shared
// lock to unique lock.
template <typename Mutex, typename Rep, typename Period>
unique_lock<Mutex> try_transition_to_unique_lock_for(
shared_lock<Mutex>& lock,
std::chrono::duration<Rep, Period> const& timeout) {
return try_transition_lock_for<unique_lock>(lock, timeout);
}
// try_transition_to_unique_lock_until(shared_lock)
//
// Wraps mutex member function try_unlock_shared_and_lock_until.
//
// Represents an eventual attempted atomic upgrade transition from shared
// lock to unique lock.
template <typename Mutex, typename Clock, typename Duration>
unique_lock<Mutex> try_transition_to_unique_lock_until(
shared_lock<Mutex>& lock,
std::chrono::time_point<Clock, Duration> const& deadline) {
return try_transition_lock_until<unique_lock>(lock, deadline);
}
// try_transition_to_upgrade_lock(shared_lock)
//
// Wraps mutex member function try_unlock_shared_and_lock_upgrade.
//
// Represents an immediate attempted atomic upgrade transition from shared
// lock to upgrade lock.
template <typename Mutex>
upgrade_lock<Mutex> try_transition_to_upgrade_lock(shared_lock<Mutex>& lock) {
return try_transition_lock<upgrade_lock>(lock);
}
// try_transition_to_upgrade_lock_for(shared_lock)
//
// Wraps mutex member function try_unlock_shared_and_lock_upgrade_for.
//
// Represents an eventual attempted atomic upgrade transition from shared
// lock to upgrade lock.
template <typename Mutex, typename Rep, typename Period>
upgrade_lock<Mutex> try_transition_to_upgrade_lock_for(
shared_lock<Mutex>& lock,
std::chrono::duration<Rep, Period> const& timeout) {
return try_transition_lock_for<upgrade_lock>(lock, timeout);
}
// try_transition_to_upgrade_lock_until(shared_lock)
//
// Wraps mutex member function try_unlock_shared_and_lock_upgrade_until.
//
// Represents an eventual attempted atomic upgrade transition from shared
// lock to upgrade lock.
template <typename Mutex, typename Clock, typename Duration>
upgrade_lock<Mutex> try_transition_to_upgrade_lock_until(
shared_lock<Mutex>& lock,
std::chrono::time_point<Clock, Duration> const& deadline) {
return try_transition_lock_until<upgrade_lock>(lock, deadline);
}
} // namespace folly
/*
* 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 <array>
#include <mutex>
#include <shared_mutex>
#include <tuple>
#include <type_traits>
#include <utility>
#include "lock.hpp"
namespace folly {
namespace detail {
template <typename, typename Mutex>
inline constexpr bool kSynchronizedMutexIsUnique = false;
template <typename Mutex>
inline constexpr bool kSynchronizedMutexIsUnique<
decltype(void(std::declval<Mutex&>().lock())),
Mutex> = true;
template <typename, typename Mutex>
inline constexpr bool kSynchronizedMutexIsShared = false;
template <typename Mutex>
inline constexpr bool kSynchronizedMutexIsShared<
decltype(void(std::declval<Mutex&>().lock_shared())),
Mutex> = true;
template <typename, typename Mutex>
inline constexpr bool kSynchronizedMutexIsUpgrade = false;
template <typename Mutex>
inline constexpr bool kSynchronizedMutexIsUpgrade<
decltype(void(std::declval<Mutex&>().lock_upgrade())),
Mutex> = true;
/**
* 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 { Unknown, Unique, Shared, Upgrade };
template <typename Mutex>
inline constexpr SynchronizedMutexLevel kSynchronizedMutexLevel =
kSynchronizedMutexIsUpgrade<void, Mutex> ? SynchronizedMutexLevel::Upgrade
: kSynchronizedMutexIsShared<void, Mutex> ? SynchronizedMutexLevel::Shared
: kSynchronizedMutexIsUnique<void, Mutex> ? SynchronizedMutexLevel::Unique
: SynchronizedMutexLevel::Unknown;
enum class SynchronizedMutexMethod { Lock, TryLock };
template <SynchronizedMutexLevel Level, SynchronizedMutexMethod Method>
struct SynchronizedLockPolicy {
static constexpr SynchronizedMutexLevel level = Level;
static constexpr SynchronizedMutexMethod method = Method;
};
using SynchronizedLockPolicyExclusive = SynchronizedLockPolicy<
SynchronizedMutexLevel::Unique,
SynchronizedMutexMethod::Lock>;
using SynchronizedLockPolicyTryExclusive = SynchronizedLockPolicy<
SynchronizedMutexLevel::Unique,
SynchronizedMutexMethod::TryLock>;
using SynchronizedLockPolicyShared = SynchronizedLockPolicy<
SynchronizedMutexLevel::Shared,
SynchronizedMutexMethod::Lock>;
using SynchronizedLockPolicyTryShared = SynchronizedLockPolicy<
SynchronizedMutexLevel::Shared,
SynchronizedMutexMethod::TryLock>;
using SynchronizedLockPolicyUpgrade = SynchronizedLockPolicy<
SynchronizedMutexLevel::Upgrade,
SynchronizedMutexMethod::Lock>;
using SynchronizedLockPolicyTryUpgrade = SynchronizedLockPolicy<
SynchronizedMutexLevel::Upgrade,
SynchronizedMutexMethod::TryLock>;
template <SynchronizedMutexLevel>
struct SynchronizedLockType_ {};
template <>
struct SynchronizedLockType_<SynchronizedMutexLevel::Unique> {
template <typename Mutex>
using apply = std::unique_lock<Mutex>;
};
template <>
struct SynchronizedLockType_<SynchronizedMutexLevel::Shared> {
template <typename Mutex>
using apply = std::shared_lock<Mutex>;
};
template <>
struct SynchronizedLockType_<SynchronizedMutexLevel::Upgrade> {
template <typename Mutex>
using apply = upgrade_lock<Mutex>;
};
template <SynchronizedMutexLevel Level, typename MutexType>
using SynchronizedLockType =
typename SynchronizedLockType_<Level>::template apply<MutexType>;
} // 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.
*/
template <class Subclass>
class SynchronizedBase<Subclass, detail::SynchronizedMutexLevel::Shared> {
private:
template <typename T, typename P>
using LockedPtr_ = ::folly::LockedPtr<T, P>;
public:
using LockPolicyExclusive = detail::SynchronizedLockPolicyExclusive;
using LockPolicyShared = detail::SynchronizedLockPolicyShared;
using LockPolicyTryExclusive = detail::SynchronizedLockPolicyTryExclusive;
using LockPolicyTryShared = detail::SynchronizedLockPolicyTryShared;
using WLockedPtr = LockedPtr_<Subclass, LockPolicyExclusive>;
using ConstWLockedPtr = LockedPtr_<const Subclass, LockPolicyExclusive>;
using RLockedPtr = LockedPtr_<Subclass, LockPolicyShared>;
using ConstRLockedPtr = LockedPtr_<const Subclass, LockPolicyShared>;
using TryWLockedPtr = LockedPtr_<Subclass, LockPolicyTryExclusive>;
using ConstTryWLockedPtr = LockedPtr_<const Subclass, LockPolicyTryExclusive>;
using TryRLockedPtr = LockedPtr_<Subclass, LockPolicyTryShared>;
using ConstTryRLockedPtr = LockedPtr_<const Subclass, LockPolicyTryShared>;
// These aliases are deprecated.
// TODO: Codemod them away.
using LockedPtr = WLockedPtr;
using ConstLockedPtr = ConstRLockedPtr;
/**
* @brief Acquire an exclusive lock.
*
* Acquire an exclusive lock, and return a LockedPtr that can be used to
* safely access the datum.
*
* LockedPtr offers operator -> and * to provide access to the datum.
* The lock will be released when the LockedPtr is destroyed.
*
* @methodset Exclusive lock
*/
LockedPtr wlock() { return LockedPtr(static_cast<Subclass*>(this)); }
ConstWLockedPtr wlock() const {
return ConstWLockedPtr(static_cast<const Subclass*>(this));
}
/**
* @brief Acquire an exclusive lock, or null.
*
* Attempts to acquire the lock in exclusive mode. If acquisition is
* unsuccessful, the returned LockedPtr will be null.
*
* (Use LockedPtr::operator bool() or LockedPtr::isNull() to check for
* validity.)
*
* @methodset Exclusive lock
*/
TryWLockedPtr tryWLock() {
return TryWLockedPtr{static_cast<Subclass*>(this)};
}
ConstTryWLockedPtr tryWLock() const {
return ConstTryWLockedPtr{static_cast<const Subclass*>(this)};
}
/**
* @brief Acquire a read lock.
*
* The returned LockedPtr will force const access to the data unless the lock
* is acquired in non-const context and asNonConstUnsafe() is used.
*
* @methodset Shared lock
*/
RLockedPtr rlock() { return RLockedPtr(static_cast<Subclass*>(this)); }
ConstLockedPtr rlock() const {
return ConstLockedPtr(static_cast<const Subclass*>(this));
}
/**
* @brief Acquire a read lock, or null.
*
* Attempts to acquire the lock in shared mode. If acquisition is
* unsuccessful, the returned LockedPtr will be null.
*
* (Use LockedPtr::operator bool() or LockedPtr::isNull() to check for
* validity.)
*
* @methodset Shared lock
*/
TryRLockedPtr tryRLock() {
return TryRLockedPtr{static_cast<Subclass*>(this)};
}
ConstTryRLockedPtr tryRLock() const {
return ConstTryRLockedPtr{static_cast<const Subclass*>(this)};
}
/**
* Attempts to acquire the lock, or fails if the timeout elapses first.
* If acquisition is unsuccessful, the returned LockedPtr will be null.
*
* (Use LockedPtr::operator bool() or LockedPtr::isNull() to check for
* validity.)
*
* @methodset Exclusive lock
*/
template <class Rep, class Period>
LockedPtr wlock(const std::chrono::duration<Rep, Period>& timeout) {
return LockedPtr(static_cast<Subclass*>(this), timeout);
}
template <class Rep, class Period>
LockedPtr wlock(const std::chrono::duration<Rep, Period>& timeout) const {
return LockedPtr(static_cast<const Subclass*>(this), timeout);
}
/**
* Attempts to acquire the lock, or fails if the timeout elapses first.
* If acquisition is unsuccessful, the returned LockedPtr will be null.
*
* (Use LockedPtr::operator bool() or LockedPtr::isNull() to check for
* validity.)
*
* @methodset Shared lock
*/
template <class Rep, class Period>
RLockedPtr rlock(const std::chrono::duration<Rep, Period>& timeout) {
return RLockedPtr(static_cast<Subclass*>(this), timeout);
}
template <class Rep, class Period>
ConstRLockedPtr rlock(
const std::chrono::duration<Rep, Period>& timeout) const {
return ConstRLockedPtr(static_cast<const Subclass*>(this), timeout);
}
/**
* Invoke a function while holding the lock exclusively.
*
* A reference to the datum will be passed into the function as its only
* argument.
*
* This can be used with a lambda argument for easily defining small critical
* sections in the code. For example:
*
* auto value = obj.withWLock([](auto& data) {
* data.doStuff();
* return data.getValue();
* });
*
* @methodset Exclusive lock
*/
template <class Function>
auto withWLock(Function&& function) {
return function(*wlock());
}
template <class Function>
auto withWLock(Function&& function) const {
return function(*wlock());
}
/**
* Invoke a function while holding the lock exclusively.
*
* This is similar to withWLock(), but the function will be passed a
* LockedPtr rather than a reference to the data itself.
*
* This allows scopedUnlock() to be called on the LockedPtr argument if
* desired.
*
* @methodset Exclusive lock
*/
template <class Function>
auto withWLockPtr(Function&& function) {
return function(wlock());
}
template <class Function>
auto withWLockPtr(Function&& function) const {
return function(wlock());
}
/**
* Invoke a function while holding an the lock in shared mode.
*
* A const reference to the datum will be passed into the function as its
* only argument.
*
* @methodset Shared lock
*/
template <class Function>
auto withRLock(Function&& function) const {
return function(*rlock());
}
/**
* Invoke a function while holding the lock in shared mode.
*
* This is similar to withRLock(), but the function will be passed a
* LockedPtr rather than a reference to the data itself.
*
* This allows scopedUnlock() to be called on the LockedPtr argument if
* desired.
*
* @methodset Shared lock
*/
template <class Function>
auto withRLockPtr(Function&& function) {
return function(rlock());
}
template <class Function>
auto withRLockPtr(Function&& function) const {
return function(rlock());
}
};
/**
* 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
*/
template <class Subclass>
class SynchronizedBase<Subclass, detail::SynchronizedMutexLevel::Upgrade>
: public SynchronizedBase<
Subclass,
detail::SynchronizedMutexLevel::Shared> {
private:
template <typename T, typename P>
using LockedPtr_ = ::folly::LockedPtr<T, P>;
public:
using LockPolicyUpgrade = detail::SynchronizedLockPolicyUpgrade;
using LockPolicyTryUpgrade = detail::SynchronizedLockPolicyTryUpgrade;
using UpgradeLockedPtr = LockedPtr_<Subclass, LockPolicyUpgrade>;
using ConstUpgradeLockedPtr = LockedPtr_<const Subclass, LockPolicyUpgrade>;
using TryUpgradeLockedPtr = LockedPtr_<Subclass, LockPolicyTryUpgrade>;
using ConstTryUpgradeLockedPtr =
LockedPtr_<const Subclass, LockPolicyTryUpgrade>;
/**
* @brief Acquire an upgrade lock.
*
* The returned LockedPtr will have force const access to the data unless the
* lock is acquired in non-const context and asNonConstUnsafe() is used.
*
* @methodset Upgrade lock
*/
UpgradeLockedPtr ulock() {
return UpgradeLockedPtr(static_cast<Subclass*>(this));
}
ConstUpgradeLockedPtr ulock() const {
return ConstUpgradeLockedPtr(static_cast<const Subclass*>(this));
}
/**
* @brief Acquire an upgrade lock, or null.
*
* Attempts to acquire the lock in upgrade mode. If acquisition is
* unsuccessful, the returned LockedPtr will be null.
*
* (Use LockedPtr::operator bool() or LockedPtr::isNull() to check for
* validity.)
*
* @methodset Upgrade lock
*/
TryUpgradeLockedPtr tryULock() {
return TryUpgradeLockedPtr{static_cast<Subclass*>(this)};
}
/**
* Acquire an upgrade lock and return a LockedPtr that can be used to safely
* access the datum
*
* And the const version
*
* @methodset Upgrade lock
*/
template <class Rep, class Period>
UpgradeLockedPtr ulock(const std::chrono::duration<Rep, Period>& timeout) {
return UpgradeLockedPtr(static_cast<Subclass*>(this), timeout);
}
/**
* Invoke a function while holding the lock.
*
* A reference to the datum will be passed into the function as its only
* argument.
*
* This can be used with a lambda argument for easily defining small critical
* sections in the code. For example:
*
* auto value = obj.withULock([](auto& data) {
* data.doStuff();
* return data.getValue();
* });
*
* This is probably not the function you want. If the intent is to read the
* data object and determine whether you should upgrade to a write lock then
* the withULockPtr() method should be called instead, since it gives access
* to the LockedPtr proxy (which can be upgraded via the
* moveFromUpgradeToWrite() method)
*
* @methodset Upgrade lock
*/
template <class Function>
auto withULock(Function&& function) {
return function(*ulock());
}
template <class Function>
auto withULock(Function&& function) const {
return function(*ulock());
}
/**
* Invoke a function while holding the lock exclusively.
*
* This is similar to withULock(), but the function will be passed a
* LockedPtr rather than a reference to the data itself.
*
* This allows scopedUnlock() and as_lock() to be called on the
* LockedPtr argument.
*
* This also allows you to upgrade the LockedPtr proxy to a write state so
* that changes can be made to the underlying data
*
* @methodset Upgrade lock
*/
template <class Function>
auto withULockPtr(Function&& function) {
return function(ulock());
}
template <class Function>
auto withULockPtr(Function&& function) const {
return function(ulock());
}
};
/**
* SynchronizedBase specialization for non-shared mutex types.
*
* This class provides lock() methods for acquiring the lock and accessing the
* data.
*/
template <class Subclass>
class SynchronizedBase<Subclass, detail::SynchronizedMutexLevel::Unique> {
private:
template <typename T, typename P>
using LockedPtr_ = ::folly::LockedPtr<T, P>;
public:
using LockPolicyExclusive = detail::SynchronizedLockPolicyExclusive;
using LockPolicyTryExclusive = detail::SynchronizedLockPolicyTryExclusive;
using LockedPtr = LockedPtr_<Subclass, LockPolicyExclusive>;
using ConstLockedPtr = LockedPtr_<const Subclass, LockPolicyExclusive>;
using TryLockedPtr = LockedPtr_<Subclass, LockPolicyTryExclusive>;
using ConstTryLockedPtr = LockedPtr_<const Subclass, LockPolicyTryExclusive>;
/**
* @brief Acquire the lock.
*
* Return a LockedPtr that can be used to safely access the datum.
*
* @methodset Non-shareable lock
*/
LockedPtr lock() { return LockedPtr(static_cast<Subclass*>(this)); }
/**
* Acquire a lock, and return a ConstLockedPtr that can be used to safely
* access the datum.
*
* @methodset Non-shareable lock
*/
ConstLockedPtr lock() const {
return ConstLockedPtr(static_cast<const Subclass*>(this));
}
/**
* @brief Acquire the lock, or null.
*
* Attempts to acquire the lock in exclusive mode. If acquisition is
* unsuccessful, the returned LockedPtr will be null.
*
* (Use LockedPtr::operator bool() or LockedPtr::isNull() to check for
* validity.)
*
* @methodset Non-shareable lock
*/
TryLockedPtr tryLock() { return TryLockedPtr{static_cast<Subclass*>(this)}; }
ConstTryLockedPtr tryLock() const {
return ConstTryLockedPtr{static_cast<const Subclass*>(this)};
}
/**
* Attempts to acquire the lock, or fails if the timeout elapses first.
* If acquisition is unsuccessful, the returned LockedPtr will be null.
*
* @methodset Non-shareable lock
*/
template <class Rep, class Period>
LockedPtr lock(const std::chrono::duration<Rep, Period>& timeout) {
return LockedPtr(static_cast<Subclass*>(this), timeout);
}
/**
* Attempts to acquire the lock, or fails if the timeout elapses first.
* If acquisition is unsuccessful, the returned LockedPtr will be null.
*
* @methodset Non-shareable lock
*/
template <class Rep, class Period>
ConstLockedPtr lock(const std::chrono::duration<Rep, Period>& timeout) const {
return ConstLockedPtr(static_cast<const Subclass*>(this), timeout);
}
/**
* Invoke a function while holding the lock.
*
* A reference to the datum will be passed into the function as its only
* argument.
*
* This can be used with a lambda argument for easily defining small critical
* sections in the code. For example:
*
* auto value = obj.withLock([](auto& data) {
* data.doStuff();
* return data.getValue();
* });
*
* @methodset Non-shareable lock
*/
template <class Function>
auto withLock(Function&& function) {
return function(*lock());
}
template <class Function>
auto withLock(Function&& function) const {
return function(*lock());
}
/**
* Invoke a function while holding the lock exclusively.
*
* This is similar to withWLock(), but the function will be passed a
* LockedPtr rather than a reference to the data itself.
*
* This allows scopedUnlock() and as_lock() to be called on the
* LockedPtr argument.
*
* @methodset Non-shareable lock
*/
template <class Function>
auto withLockPtr(Function&& function) {
return function(lock());
}
template <class Function>
auto withLockPtr(Function&& function) const {
return function(lock());
}
};
/**
* `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 = std::shared_mutex>
struct Synchronized : public SynchronizedBase<
Synchronized<T, Mutex>,
detail::kSynchronizedMutexLevel<Mutex>> {
private:
using Base = SynchronizedBase<
Synchronized<T, Mutex>,
detail::kSynchronizedMutexLevel<Mutex>>;
static constexpr bool nxCopyCtor{
std::is_nothrow_copy_constructible<T>::value};
static constexpr bool nxMoveCtor{
std::is_nothrow_move_constructible<T>::value};
// used to disable copy construction and assignment
class NonImplementedType;
public:
using LockedPtr = typename Base::LockedPtr;
using ConstLockedPtr = typename Base::ConstLockedPtr;
using DataType = T;
using MutexType = Mutex;
/**
* Default constructor leaves both members call their own default constructor.
*/
constexpr Synchronized() = default;
public:
/**
* Copy constructor. Enabled only when the data type is copy-constructible.
*
* Takes a shared-or-exclusive lock on the source mutex while performing the
* copy-construction of the destination data from the source data. No lock is
* taken on the destination mutex.
*
* May throw even when the data type is is nothrow-copy-constructible because
* acquiring a lock may throw.
*
* deprecated
*/
/* implicit */ Synchronized(typename std::conditional<
std::is_copy_constructible<T>::value,
const Synchronized&,
NonImplementedType>::type rhs) /* may throw */
: Synchronized(rhs.copy()) {}
/**
* Move-constructs from the source data without locking either the source or
* the destination mutex.
*
* Semantically, assumes that the source object is a true rvalue and therefore
* that no synchronization is required for accessing it.
*
* deprecated
*/
Synchronized(Synchronized&& rhs) noexcept(nxMoveCtor)
: Synchronized(std::move(rhs.datum_)) {}
/**
* Constructor taking a datum as argument copies it. There is no
* need to lock the constructing object.
*/
explicit Synchronized(const T& rhs) noexcept(nxCopyCtor) : datum_(rhs) {}
/**
* Constructor taking a datum rvalue as argument moves it. There is no need
* to lock the constructing object.
*/
explicit Synchronized(T&& rhs) noexcept(nxMoveCtor)
: datum_(std::move(rhs)) {}
/**
* Lets you construct non-movable types in-place. Use the constexpr
* instance `in_place` as the first argument.
*/
template <typename... Args>
explicit constexpr Synchronized(std::in_place_t, Args&&... args)
: datum_(std::forward<Args>(args)...) {}
/**
* Lets you construct the synchronized object and also pass construction
* parameters to the underlying mutex if desired
*/
template <typename... DatumArgs, typename... MutexArgs>
Synchronized(
std::piecewise_construct_t,
std::tuple<DatumArgs...> datumArgs,
std::tuple<MutexArgs...> mutexArgs)
: Synchronized{
std::piecewise_construct,
std::move(datumArgs),
std::move(mutexArgs),
std::make_index_sequence<sizeof...(DatumArgs)>{},
std::make_index_sequence<sizeof...(MutexArgs)>{}} {}
/**
* Copy assignment operator.
*
* Enabled only when the data type is copy-constructible and move-assignable.
*
* Move-assigns from a copy of the source data.
*
* Takes a shared-or-exclusive lock on the source mutex while copying the
* source data to a temporary. Takes an exclusive lock on the destination
* mutex while move-assigning from the temporary.
*
* This technique consts an extra temporary but avoids the need to take locks
* on both mutexes together.
*
* deprecated
*/
Synchronized& operator=(typename std::conditional<
std::is_copy_constructible<T>::value &&
std::is_move_assignable<T>::value,
const Synchronized&,
NonImplementedType>::type rhs) {
return *this = rhs.copy();
}
/**
* Move assignment operator.
*
* Takes an exclusive lock on the destination mutex while move-assigning the
* destination data from the source data. The source mutex is not locked or
* otherwise accessed.
*
* Semantically, assumes that the source object is a true rvalue and therefore
* that no synchronization is required for accessing it.
*
* deprecated
*/
Synchronized& operator=(Synchronized&& rhs) {
return *this = std::move(rhs.datum_);
}
/**
* Lock object, assign datum.
*/
Synchronized& operator=(const T& rhs) {
if (&datum_ != &rhs) {
auto guard = LockedPtr{this};
datum_ = rhs;
}
return *this;
}
/**
* Lock object, move-assign datum.
*/
Synchronized& operator=(T&& rhs) {
if (&datum_ != &rhs) {
auto guard = LockedPtr{this};
datum_ = std::move(rhs);
}
return *this;
}
/**
* @brief Acquire some lock.
*
* If the mutex is a shared mutex, and the Synchronized instance is const,
* this acquires a shared lock. Otherwise this acquires an exclusive lock.
*
* In general, prefer using the explicit rlock() and wlock() methods
* for read-write locks, and lock() for purely exclusive locks.
*
* contextualLock() is primarily intended for use in other template functions
* that do not necessarily know the lock type.
*/
LockedPtr contextualLock() { return LockedPtr(this); }
ConstLockedPtr contextualLock() const { return ConstLockedPtr(this); }
template <class Rep, class Period>
LockedPtr contextualLock(const std::chrono::duration<Rep, Period>& timeout) {
return LockedPtr(this, timeout);
}
template <class Rep, class Period>
ConstLockedPtr contextualLock(
const std::chrono::duration<Rep, Period>& timeout) const {
return ConstLockedPtr(this, timeout);
}
/**
* @brief Acquire a lock for reading.
*
* contextualRLock() acquires a read lock if the mutex type is shared,
* or a regular exclusive lock for non-shared mutex types.
*
* contextualRLock() when you know that you prefer a read lock (if
* available), even if the Synchronized<T> object itself is non-const.
*/
ConstLockedPtr contextualRLock() const { return ConstLockedPtr(this); }
template <class Rep, class Period>
ConstLockedPtr contextualRLock(
const std::chrono::duration<Rep, Period>& timeout) const {
return ConstLockedPtr(this, timeout);
}
/**
* @brief Access the datum under lock.
*
* deprecated
*
* This accessor offers a LockedPtr. In turn, LockedPtr offers
* operator-> returning a pointer to T. The operator-> keeps
* expanding until it reaches a pointer, so syncobj->foo() will lock
* the object and call foo() against it.
*
* NOTE: This API is planned to be deprecated in an upcoming diff.
* Prefer using lock(), wlock(), or rlock() instead.
*/
[[deprecated("use explicit lock(), wlock(), or rlock() instead")]] LockedPtr
operator->() {
return LockedPtr(this);
}
/**
* deprecated
*
* Obtain a ConstLockedPtr.
*
* NOTE: This API is planned to be deprecated in an upcoming diff.
* Prefer using lock(), wlock(), or rlock() instead.
*/
[[deprecated(
"use explicit lock(), wlock(), or rlock() instead")]] ConstLockedPtr
operator->() const {
return ConstLockedPtr(this);
}
/**
* @brief Acquire a LockedPtr with timeout.
*
* Attempts to acquire for a given number of milliseconds. If
* acquisition is unsuccessful, the returned LockedPtr is nullptr.
*
* NOTE: This API is deprecated. Use lock(), wlock(), or rlock() instead.
* In the future it will be marked with a deprecation attribute to emit
* build-time warnings, and then it will be removed entirely.
*/
LockedPtr timedAcquire(unsigned int milliseconds) {
return LockedPtr(this, std::chrono::milliseconds(milliseconds));
}
/**
* Attempts to acquire for a given number of milliseconds. If
* acquisition is unsuccessful, the returned ConstLockedPtr is nullptr.
*
* NOTE: This API is deprecated. Use lock(), wlock(), or rlock() instead.
* In the future it will be marked with a deprecation attribute to emit
* build-time warnings, and then it will be removed entirely.
*/
ConstLockedPtr timedAcquire(unsigned int milliseconds) const {
return ConstLockedPtr(this, std::chrono::milliseconds(milliseconds));
}
/**
* @brief Swap datum.
*
* Swaps with another Synchronized. Protected against
* self-swap. Only data is swapped. Locks are acquired in increasing
* address order.
*/
void swap(Synchronized& rhs) {
if (this == &rhs) {
return;
}
if (this > &rhs) {
return rhs.swap(*this);
}
auto guard1 = LockedPtr{this};
auto guard2 = LockedPtr{&rhs};
using std::swap;
swap(datum_, rhs.datum_);
}
/**
* Swap with another datum. Recommended because it keeps the mutex
* held only briefly.
*/
void swap(T& rhs) {
LockedPtr guard(this);
using std::swap;
swap(datum_, rhs);
}
/**
* @brief Exchange datum.
*
* Assign another datum and return the original value. Recommended
* because it keeps the mutex held only briefly.
*/
T exchange(T&& rhs) {
swap(rhs);
return std::move(rhs);
}
/**
* Copies datum to a given target.
*/
void copyInto(T& target) const {
ConstLockedPtr guard(this);
target = datum_;
}
/**
* Returns a fresh copy of the datum.
*/
T copy() const {
ConstLockedPtr guard(this);
return datum_;
}
/**
* @brief Access datum without locking.
*
* Returns a reference to the datum without acquiring a lock.
*
* Provided as a backdoor for call-sites where it is known safe to be used.
* For example, when it is known that only one thread has access to the
* Synchronized instance.
*
* To be used with care - this method explicitly overrides the normal safety
* guarantees provided by the rest of the Synchronized API.
*/
T& unsafeGetUnlocked() { return datum_; }
const T& unsafeGetUnlocked() const { return datum_; }
private:
template <class LockedType, class MutexType, class LockPolicy>
friend class folly::LockedPtrBase;
template <class LockedType, class LockPolicy>
friend class folly::LockedPtr;
/**
* Helper constructors to enable Synchronized for
* non-default constructible types T.
* Guards are created in actual public constructors and are alive
* for the time required to construct the object
*/
Synchronized(
const Synchronized& rhs,
const ConstLockedPtr& /*guard*/) noexcept(nxCopyCtor)
: datum_(rhs.datum_) {}
Synchronized(Synchronized&& rhs, const LockedPtr& /*guard*/) noexcept(
nxMoveCtor)
: datum_(std::move(rhs.datum_)) {}
template <
typename... DatumArgs,
typename... MutexArgs,
std::size_t... IndicesOne,
std::size_t... IndicesTwo>
Synchronized(
std::piecewise_construct_t,
std::tuple<DatumArgs...> datumArgs,
std::tuple<MutexArgs...> mutexArgs,
std::index_sequence<IndicesOne...>,
std::index_sequence<IndicesTwo...>)
: datum_{std::get<IndicesOne>(std::move(datumArgs))...},
mutex_{std::get<IndicesTwo>(std::move(mutexArgs))...} {}
// simulacrum of data members - keep data members in sync!
// LockedPtr needs offsetof() which is specified only for standard-layout
// types which Synchronized is not so we define a simulacrum for offsetof
struct Simulacrum {
std::aligned_storage_t<sizeof(DataType), alignof(DataType)> datum_;
std::aligned_storage_t<sizeof(MutexType), alignof(MutexType)> mutex_;
};
// data members - keep simulacrum of data members in sync!
T datum_;
mutable Mutex 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 = std::shared_mutex>
struct [[deprecated(
"use Synchronized and explicit lock(), wlock(), or rlock() instead")]] ImplicitSynchronized
: Synchronized<T, Mutex> {
private:
using Base = Synchronized<T, Mutex>;
public:
using LockedPtr = typename Base::LockedPtr;
using ConstLockedPtr = typename Base::ConstLockedPtr;
using DataType = typename Base::DataType;
using MutexType = typename Base::MutexType;
using Base::Base;
using Base::operator=;
};
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.
*/
template <class SynchronizedType, bool AllowsConcurrentAccess>
using SynchronizedDataType = typename std::conditional<
AllowsConcurrentAccess || std::is_const<SynchronizedType>::value,
typename SynchronizedType::DataType const,
typename SynchronizedType::DataType>::type;
/*
* 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.
*/
template <class SynchronizedType>
using LockedPtrType = typename std::conditional<
std::is_const<SynchronizedType>::value,
typename SynchronizedType::ConstLockedPtr,
typename SynchronizedType::LockedPtr>::type;
template <
typename Synchronized,
typename LockFunc,
typename TryLockFunc,
typename... Args>
class SynchronizedLocker {
public:
using LockedPtr = std::invoke_result_t<LockFunc&, Synchronized&, const Args&...>;
template <typename LockFuncType, typename TryLockFuncType, typename... As>
SynchronizedLocker(
Synchronized& sync,
LockFuncType&& lockFunc,
TryLockFuncType tryLockFunc,
As&&... as)
: synchronized{sync},
lockFunc_{std::forward<LockFuncType>(lockFunc)},
tryLockFunc_{std::forward<TryLockFuncType>(tryLockFunc)},
args_{std::forward<As>(as)...} {}
auto lock() const {
auto args = std::tuple<const Args&...>{args_};
return apply(lockFunc_, std::tuple_cat(std::tie(synchronized), args));
}
auto tryLock() const { return tryLockFunc_(synchronized); }
private:
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
* 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...> {
// capture the list of lockers as a tuple
auto lockers = std::forward_as_tuple(lockersIn...);
// make a list of null LockedPtr instances that we will return to the caller
auto lockedPtrs = std::tuple<typename SynchronizedLocker::LockedPtr...>{};
// start by locking the first thing in the list
std::get<0>(lockedPtrs) = std::get<0>(lockers).lock();
auto indexLocked = 0;
while (true) {
auto couldLockAll = true;
auto index = 0;
for (auto &locker : lockers)
{
// if we should try_lock on the current locker then do so
if (index != indexLocked) {
auto lockedPtr = locker.tryLock();
// if we were unable to lock this mutex,
//
// 1. release all the locks,
// 2. yield control to another thread to be nice
// 3. block on the mutex we failed to lock, acquire the lock
// 4. break out and set the index of the current mutex to indicate
// which mutex we have locked
if (!lockedPtr) {
// writing lockedPtrs = decltype(lockedPtrs){} does not compile on
// gcc, I believe this is a bug D7676798
lockedPtrs = std::tuple<typename SynchronizedLocker::LockedPtr...>{};
std::this_thread::yield();
fetch(lockedPtrs, index) = locker.lock();
indexLocked = index;
couldLockAll = false;
break;
}
// else store the locked mutex in the list we return
fetch(lockedPtrs, index) = std::move(lockedPtr);
}
index++;
}
if (couldLockAll) {
return lockedPtrs;
}
}
}
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
/**
* This class temporarily unlocks a LockedPtr in a scoped manner.
*/
template <class SynchronizedType, class LockPolicy>
class ScopedUnlocker {
public:
explicit ScopedUnlocker(LockedPtr<SynchronizedType, LockPolicy>* p) noexcept
: ptr_(p), parent_(p->parent()) {
ptr_->releaseLock();
}
ScopedUnlocker(const ScopedUnlocker&) = delete;
ScopedUnlocker& operator=(const ScopedUnlocker&) = delete;
ScopedUnlocker(ScopedUnlocker&& other) noexcept
: ptr_(std::exchange(other.ptr_, nullptr)),
parent_(std::exchange(other.parent_, nullptr)) {}
ScopedUnlocker& operator=(ScopedUnlocker&& other) = delete;
~ScopedUnlocker() noexcept(false) {
if (ptr_) {
ptr_->reacquireLock(parent_);
}
}
private:
LockedPtr<SynchronizedType, LockPolicy>* ptr_{nullptr};
SynchronizedType* parent_{nullptr};
};
/**
* 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 {
private:
constexpr static bool AllowsConcurrentAccess =
LockPolicy::level != detail::SynchronizedMutexLevel::Unique;
using CDataType = // the DataType with the appropriate const-qualification
detail::SynchronizedDataType<SynchronizedType, AllowsConcurrentAccess>;
template <typename LockPolicyOther>
using EnableIfSameLevel =
std::enable_if_t<LockPolicy::level == LockPolicyOther::level>;
template <typename, typename>
friend class LockedPtr;
friend class ScopedUnlocker<SynchronizedType, LockPolicy>;
public:
using DataType = typename SynchronizedType::DataType;
using MutexType = typename SynchronizedType::MutexType;
using Synchronized = typename std::remove_const<SynchronizedType>::type;
using LockType = detail::SynchronizedLockType<LockPolicy::level, MutexType>;
/**
* Creates an uninitialized LockedPtr.
*
* Dereferencing an uninitialized LockedPtr is not allowed.
*/
LockedPtr() = default;
/**
* Takes a Synchronized<T> and locks it.
*/
explicit LockedPtr(SynchronizedType* parent)
: lock_{!parent ? LockType{} : doLock(parent->mutex_)} {}
/**
* Takes a Synchronized<T> and attempts to lock it, within the specified
* timeout.
*
* Blocks until the lock is acquired or until the specified timeout expires.
* If the timeout expired without acquiring the lock, the LockedPtr will be
* null, and LockedPtr::isNull() will return true.
*/
template <class Rep, class Period>
LockedPtr(
SynchronizedType* parent,
const std::chrono::duration<Rep, Period>& timeout)
: lock_{parent ? LockType{parent->mutex_, timeout} : LockType{}} {}
/**
* Move constructor.
*/
LockedPtr(LockedPtr&& rhs) noexcept = default;
template <
typename Type = SynchronizedType,
std::enable_if_t<std::is_const<Type>::value, int> = 0>
/* implicit */ LockedPtr(LockedPtr<Synchronized, LockPolicy>&& rhs) noexcept
: lock_{std::move(rhs.lock_)} {}
template <
typename LockPolicyType,
EnableIfSameLevel<LockPolicyType>* = nullptr>
explicit LockedPtr(
LockedPtr<SynchronizedType, LockPolicyType>&& other) noexcept
: lock_{std::move(other.lock_)} {}
template <
typename Type = SynchronizedType,
typename LockPolicyType,
std::enable_if_t<std::is_const<Type>::value, int> = 0,
EnableIfSameLevel<LockPolicyType>* = nullptr>
explicit LockedPtr(LockedPtr<Synchronized, LockPolicyType>&& rhs) noexcept
: lock_{std::move(rhs.lock_)} {}
/**
* Move assignment operator.
*/
LockedPtr& operator=(LockedPtr&& rhs) noexcept = default;
template <
typename LockPolicyType,
EnableIfSameLevel<LockPolicyType>* = nullptr>
LockedPtr& operator=(
LockedPtr<SynchronizedType, LockPolicyType>&& other) noexcept {
lock_ = std::move(other.lock_);
return *this;
}
template <
typename Type = SynchronizedType,
typename LockPolicyType,
std::enable_if_t<std::is_const<Type>::value, int> = 0,
EnableIfSameLevel<LockPolicyType>* = nullptr>
LockedPtr& operator=(
LockedPtr<Synchronized, LockPolicyType>&& other) noexcept {
lock_ = std::move(other.lock_);
return *this;
}
/*
* Copy constructor and assignment operator are deleted.
*/
LockedPtr(const LockedPtr& rhs) = delete;
LockedPtr& operator=(const LockedPtr& rhs) = delete;
/**
* Destructor releases.
*/
~LockedPtr() = default;
/**
* Access the underlying lock object.
*/
LockType& as_lock() noexcept { return lock_; }
LockType const& as_lock() const noexcept { return lock_; }
/**
* Check if this LockedPtr is uninitialized, or points to valid locked data.
*
* This method can be used to check if a timed-acquire operation succeeded.
* If an acquire operation times out it will result in a null LockedPtr.
*
* A LockedPtr is always either null, or holds a lock to valid data.
* Methods such as scopedUnlock() reset the LockedPtr to null for the
* duration of the unlock.
*/
bool isNull() const { return !lock_.owns_lock(); }
/**
* Explicit boolean conversion.
*
* Returns !isNull()
*/
explicit operator bool() const { return lock_.owns_lock(); }
/**
* Access the locked data.
*
* This method should only be used if the LockedPtr is valid.
*/
CDataType* operator->() const { return std::addressof(parent()->datum_); }
/**
* Access the locked data.
*
* This method should only be used if the LockedPtr is valid.
*/
CDataType& operator*() const { return parent()->datum_; }
void unlock() noexcept { lock_ = {}; }
/**
* Locks that allow concurrent access (shared, upgrade) force const
* access with the standard accessors even if the Synchronized
* object is non-const.
*
* In some cases non-const access can be needed, for example:
*
* - Under an upgrade lock, to get references that will be mutated
* after upgrading to a write lock.
*
* - Under an read lock, if some mutating operations on the data
* are thread safe (e.g. mutating the value in an associative
* container with reference stability).
*
* asNonConstUnsafe() returns a non-const reference to the data if
* the parent Synchronized object was non-const at the point of lock
* acquisition.
*/
template <typename = void>
DataType& asNonConstUnsafe() const {
static_assert(
AllowsConcurrentAccess && !std::is_const<SynchronizedType>::value,
"asNonConstUnsafe() is only available on non-exclusive locks"
" acquired in a non-const context");
return parent()->datum_;
}
/**
* Temporarily unlock the LockedPtr, and reset it to null.
*
* Returns an helper object that will re-lock and restore the LockedPtr when
* the helper is destroyed. The LockedPtr may not be dereferenced for as
* long as this helper object exists.
*/
ScopedUnlocker<SynchronizedType, LockPolicy> scopedUnlock() {
return ScopedUnlocker<SynchronizedType, LockPolicy>(this);
}
/***************************************************************************
* Upgrade lock methods.
* These are disabled via SFINAE when the mutex is not an upgrade mutex.
**************************************************************************/
/**
* Move the locked ptr from an upgrade state to an exclusive state. The
* current lock is left in a null state.
*/
template <
typename SyncType = SynchronizedType,
decltype(void(std::declval<typename SyncType::MutexType&>()
.lock_upgrade()))* = nullptr>
LockedPtr<SynchronizedType, detail::SynchronizedLockPolicyExclusive>
moveFromUpgradeToWrite() {
static_assert(std::is_same<SyncType, SynchronizedType>::value, "mismatch");
return transition_to_unique_lock(lock_);
}
/**
* Move the locked ptr from an exclusive state to an upgrade state. The
* current lock is left in a null state.
*/
template <
typename SyncType = SynchronizedType,
decltype(void(std::declval<typename SyncType::MutexType&>()
.lock_upgrade()))* = nullptr>
LockedPtr<SynchronizedType, detail::SynchronizedLockPolicyUpgrade>
moveFromWriteToUpgrade() {
static_assert(std::is_same<SyncType, SynchronizedType>::value, "mismatch");
return transition_to_upgrade_lock(lock_);
}
/**
* Move the locked ptr from an upgrade state to a shared state. The
* current lock is left in a null state.
*/
template <
typename SyncType = SynchronizedType,
decltype(void(std::declval<typename SyncType::MutexType&>()
.lock_upgrade()))* = nullptr>
LockedPtr<SynchronizedType, detail::SynchronizedLockPolicyShared>
moveFromUpgradeToRead() {
static_assert(std::is_same<SyncType, SynchronizedType>::value, "mismatch");
return transition_to_shared_lock(lock_);
}
/**
* Move the locked ptr from an exclusive state to a shared state. The
* current lock is left in a null state.
*/
template <
typename SyncType = SynchronizedType,
decltype(void(std::declval<typename SyncType::MutexType&>()
.lock_shared()))* = nullptr>
LockedPtr<SynchronizedType, detail::SynchronizedLockPolicyShared>
moveFromWriteToRead() {
static_assert(std::is_same<SyncType, SynchronizedType>::value, "mismatch");
return transition_to_shared_lock(lock_);
}
SynchronizedType* parent() const {
using simulacrum = typename SynchronizedType::Simulacrum;
static_assert(sizeof(simulacrum) == sizeof(SynchronizedType), "mismatch");
static_assert(alignof(simulacrum) == alignof(SynchronizedType), "mismatch");
constexpr auto off = offsetof(simulacrum, mutex_);
const auto raw = reinterpret_cast<char*>(lock_.mutex());
return reinterpret_cast<SynchronizedType*>(raw - (raw ? off : 0));
}
private:
/* implicit */ LockedPtr(LockType lock) noexcept : lock_{std::move(lock)} {}
template <typename LP>
static constexpr bool is_try =
LP::method == detail::SynchronizedMutexMethod::TryLock;
template <
typename MT,
typename LT = LockType,
typename LP = LockPolicy,
std::enable_if_t<is_try<LP>, int> = 0>
inline static LT doLock(MT& mutex) {
return LT{mutex, std::try_to_lock};
}
template <
typename MT,
typename LT = LockType,
typename LP = LockPolicy,
std::enable_if_t<!is_try<LP>, int> = 0>
inline static LT doLock(MT& mutex) {
return LT{mutex};
}
void releaseLock() noexcept {
DCHECK(lock_.owns_lock());
lock_ = {};
}
void reacquireLock(SynchronizedType* parent) {
DCHECK(parent);
DCHECK(!lock_.owns_lock());
lock_ = doLock(parent->mutex_);
}
LockType lock_;
};
/**
* 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 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
* 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) {
auto locker = [](auto& lockable) {
using Lockable = std::remove_reference_t<decltype(lockable)>;
return detail::makeSynchronizedLocker(
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
// function above
for (auto &lock : locks)
lock.release();
}
/**
* 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) {
if (static_cast<const void*>(&l1) < static_cast<const void*>(&l2)) {
auto p1 = l1.contextualLock();
auto p2 = l2.contextualLock();
return std::make_tuple(std::move(p1), std::move(p2));
} else {
auto p2 = l2.contextualLock();
auto p1 = l1.contextualLock();
return std::make_tuple(std::move(p1), std::move(p2));
}
}
/**
* 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) {
auto lockedPtrs = acquireLocked(l1, l2);
return {
std::move(std::get<0>(lockedPtrs)), std::move(std::get<1>(lockedPtrs))};
}
/************************************************************************
* 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) {
lhs.swap(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) FB_CONCATENATE(SYNCHRONIZED_##var##_, __LINE__)
namespace detail {
struct [[deprecated(
"use explicit lock(), wlock(), or rlock() instead")]] SYNCHRONIZED_macro_is_deprecated{};
}
/**
* 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(...) \
FOLLY_PUSH_WARNING \
FOLLY_GNU_DISABLE_WARNING("-Wshadow") \
FOLLY_MSVC_DISABLE_WARNING(4189) /* initialized but unreferenced */ \
FOLLY_MSVC_DISABLE_WARNING(4456) /* declaration hides local */ \
FOLLY_MSVC_DISABLE_WARNING(4457) /* declaration hides parameter */ \
FOLLY_MSVC_DISABLE_WARNING(4458) /* declaration hides member */ \
FOLLY_MSVC_DISABLE_WARNING(4459) /* declaration hides global */ \
FOLLY_GCC_DISABLE_NEW_SHADOW_WARNINGS \
if (bool SYNCHRONIZED_VAR(state) = false) { \
(void)::folly::detail::SYNCHRONIZED_macro_is_deprecated{}; \
} else \
for (auto SYNCHRONIZED_VAR(lockedPtr) = \
(FB_VA_GLUE(FB_ARG_2_OR_1, (__VA_ARGS__))).contextualLock(); \
!SYNCHRONIZED_VAR(state); \
SYNCHRONIZED_VAR(state) = true) \
for (auto& FB_VA_GLUE(FB_ARG_1, (__VA_ARGS__)) = \
*SYNCHRONIZED_VAR(lockedPtr).operator->(); \
!SYNCHRONIZED_VAR(state); \
SYNCHRONIZED_VAR(state) = true) \
FOLLY_POP_WARNING
/**
* 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(...) \
SYNCHRONIZED( \
FB_VA_GLUE(FB_ARG_1, (__VA_ARGS__)), \
as_const(FB_VA_GLUE(FB_ARG_2_OR_1, (__VA_ARGS__))))
/**
* 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) \
if (bool SYNCHRONIZED_VAR(state) = false) { \
(void)::folly::detail::SYNCHRONIZED_macro_is_deprecated{}; \
} else \
for (auto SYNCHRONIZED_VAR(ptrs) = acquireLockedPair(e1, e2); \
!SYNCHRONIZED_VAR(state); \
SYNCHRONIZED_VAR(state) = true) \
for (auto& n1 = *SYNCHRONIZED_VAR(ptrs).first; !SYNCHRONIZED_VAR(state); \
SYNCHRONIZED_VAR(state) = true) \
for (auto& n2 = *SYNCHRONIZED_VAR(ptrs).second; \
!SYNCHRONIZED_VAR(state); \
SYNCHRONIZED_VAR(state) = true)
} /* namespace folly */
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment