/* * 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 #include #include #include #include #include #include #include #include #include namespace folly { // thread_cached_synchronized // // Roughly equivalent to Synchronized, but with a per-thread cache for // acceleration. // // Use in hot code when Synchronized alone, with its shared lock and unlock, // would be too costly. // // Avoid when acceleration is marginal since per-thread caches are expensive. // // Example: // // struct writer_and_readers { // folly::thread_cached_synchronized> obj_; // std::jthread background_writer_{std::bind(loop_update, this)}; // // std::shared_ptr get_recent_data_fast() { return obj; } // // data fetch_recent_data(); // bool needs_data_and_not_signaled_done(); // void loop_update() { // while (needs_data_and_not_signaled_done()) { // obj.exchange(folly::copy_to_shared_ptr(fetch_recent_data())); // } // } // }; // // Note: // A singleton variation of this with SingletonThreadLocal would remove one // of the branches when looking up the per-thread cache but would introduce // a new branch when looking up the global version. template class thread_cached_synchronized { static_assert(std::is_same, T>::value, "not decayed"); static_assert(std::is_copy_constructible::value, "not copy-constructible"); public: using value_type = T; private: using version_type = std::uint64_t; // 64 bits will not overflow struct truth_state { relaxed_atomic version{0}; // tiny optimization if first field Mutex mutex{}; // protects value and sometimes version value_type value; template truth_state(A&&... a) noexcept( std::is_nothrow_constructible{} && std::is_nothrow_constructible{}) : value{static_cast(a)...} {} }; struct cache_state { version_type version{0}; value_type value; template cache_state(A&&... a) // noexcept(std::is_nothrow_constructible{}) : value{static_cast(a)...} {} }; using tlp_cache_state = ThreadLocalPtr; template static constexpr bool nx = noexcept(truth_state{ in_place, FOLLY_DECLVAL(A)...}); template using if_ = std::enable_if_t; using swap_fn = access::swap_fn; public: template {}> = 0> thread_cached_synchronized() noexcept(nx<>) : truth_{in_place} {} explicit thread_cached_synchronized(value_type const& a) // noexcept(nx) : truth_{a} {} explicit thread_cached_synchronized(value_type&& a) noexcept(nx) : truth_{static_cast(a)} {} template {}> = 0> explicit thread_cached_synchronized(A&& a) noexcept(nx) : truth_{static_cast(a)} {} template {}> = 0> explicit thread_cached_synchronized(in_place_t, A&&... a) noexcept(nx) : truth_{static_cast(a)...} {} template {}> = 0> thread_cached_synchronized& operator=(A&& a) noexcept(false) { store(static_cast(a)); return *this; } template void store(A&& a = A{}) { mutate([&](auto& value) { value = static_cast(a); }); } template value_type exchange(A&& a = A{}) { return mutate([&](auto& value) { // return std::exchange(value, static_cast(a)); }); } template bool compare_exchange(value_type& expected, A&& desired) { return mutate_cx(expected, static_cast(desired)); } template void swap(A& that) noexcept(false) { mutate([&](auto& value) { access::swap(value, that); }); } template > = 0> friend void swap(thread_cached_synchronized& self, A& that) noexcept(false) { self.swap(that); } value_type const& operator*() const { return ref(); } value_type const* operator->() const { return std::addressof(ref()); } value_type load() const { return folly::as_const(ref()); } /* implicit */ operator value_type() const { return load(); } private: void invalidate_caches() { truth_.version = truth_.version + 1; // intentionally not += } // TODO: past C++17, just use if-constexpr in mutate() template < typename F, typename R = invoke_result_t, if_{}> = 0> R mutate_locked(F f) { f(truth_.value); // value first: mutation may throw invalidate_caches(); } template < typename F, typename R = invoke_result_t, if_{}> = 0> R mutate_locked(F f) { decltype(auto) ret = f(truth_.value); // value first: mutation may throw invalidate_caches(); return ret; } template > R mutate(F f) { unique_lock lock{truth_.mutex}; return mutate_locked(f); } template bool mutate_cx(value_type& expected, A&& desired) { unique_lock lock{truth_.mutex}; auto const eq = folly::as_const(truth_.value) == folly::as_const(expected); if (eq) { truth_.value = // value first: mutation may throw static_cast(desired); invalidate_caches(); } else { expected = folly::as_const(truth_.value); } return eq; } FOLLY_ERASE value_type& ref() const { auto const cache = cache_.get(); auto const unexpired = cache && cache->version == truth_.version; return FOLLY_LIKELY(unexpired) ? cache->value : get_slow(); } FOLLY_NOINLINE value_type& get_slow() const { hybrid_lock lock{truth_.mutex}; auto cache = cache_.get(); if (cache == nullptr) { cache = new cache_state{truth_.value}; // value first: copy may throw cache_.reset(cache); } else { cache->value = truth_.value; // value first: copy may throw } cache->version = truth_.version; return cache->value; } mutable truth_state truth_; mutable tlp_cache_state cache_; }; } // namespace folly