194 lines
7.6 KiB
C++
194 lines
7.6 KiB
C++
/*
|
|
* Copyright (c) Facebook, Inc. and its 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 <atomic>
|
|
#include <cstdint>
|
|
#include <memory>
|
|
#include <mutex>
|
|
|
|
#include <folly/Indestructible.h>
|
|
#include <folly/experimental/ReadMostlySharedPtr.h>
|
|
#include <folly/synchronization/Rcu.h>
|
|
|
|
namespace folly {
|
|
|
|
namespace detail {
|
|
struct AtomicReadMostlyTag;
|
|
extern Indestructible<std::mutex> atomicReadMostlyMu;
|
|
extern Indestructible<rcu_domain<AtomicReadMostlyTag>> atomicReadMostlyDomain;
|
|
} // namespace detail
|
|
|
|
/*
|
|
* What atomic_shared_ptr is to shared_ptr, AtomicReadMostlyMainPtr is to
|
|
* ReadMostlyMainPtr; it allows racy conflicting accesses to one. This gives
|
|
* true shared_ptr-like semantics, including reclamation at the point where the
|
|
* last pointer to an object goes away.
|
|
*
|
|
* It's about the same speed (slightly slower) as ReadMostlyMainPtr. The most
|
|
* significant feature they share is avoiding reader-reader contention and
|
|
* atomic RMWs in the absence of writes.
|
|
*/
|
|
template <typename T>
|
|
class AtomicReadMostlyMainPtr {
|
|
public:
|
|
AtomicReadMostlyMainPtr() : curMainPtrIndex_(0) {}
|
|
|
|
explicit AtomicReadMostlyMainPtr(std::shared_ptr<T> ptr)
|
|
: curMainPtrIndex_(0) {
|
|
mainPtrs_[0] = ReadMostlyMainPtr<T>{std::move(ptr)};
|
|
}
|
|
|
|
void operator=(std::shared_ptr<T> desired) {
|
|
store(std::move(desired));
|
|
}
|
|
|
|
bool is_lock_free() const {
|
|
return false;
|
|
}
|
|
|
|
ReadMostlySharedPtr<T> load(
|
|
std::memory_order order = std::memory_order_seq_cst) const {
|
|
auto token = detail::atomicReadMostlyDomain->lock_shared();
|
|
// Synchronization point with the store in storeLocked().
|
|
auto index = curMainPtrIndex_.load(order);
|
|
auto result = mainPtrs_[index].getShared();
|
|
detail::atomicReadMostlyDomain->unlock_shared(std::move(token));
|
|
return result;
|
|
}
|
|
|
|
void store(
|
|
std::shared_ptr<T> ptr,
|
|
std::memory_order order = std::memory_order_seq_cst) {
|
|
std::shared_ptr<T> old;
|
|
{
|
|
std::lock_guard<std::mutex> lg(*detail::atomicReadMostlyMu);
|
|
old = exchangeLocked(std::move(ptr), order);
|
|
}
|
|
// If ~T() runs (triggered by the shared_ptr refcount decrement), it's here,
|
|
// after dropping the lock. This avoids a possible (albeit esoteric)
|
|
// deadlock if ~T() modifies the AtomicReadMostlyMainPtr that used to point
|
|
// to it.
|
|
}
|
|
|
|
std::shared_ptr<T> exchange(
|
|
std::shared_ptr<T> ptr,
|
|
std::memory_order order = std::memory_order_seq_cst) {
|
|
std::lock_guard<std::mutex> lg(*detail::atomicReadMostlyMu);
|
|
return exchangeLocked(std::move(ptr), order);
|
|
}
|
|
|
|
bool compare_exchange_weak(
|
|
std::shared_ptr<T>& expected,
|
|
const std::shared_ptr<T>& desired,
|
|
std::memory_order successOrder = std::memory_order_seq_cst,
|
|
std::memory_order failureOrder = std::memory_order_seq_cst) {
|
|
return compare_exchange_strong(
|
|
expected, desired, successOrder, failureOrder);
|
|
}
|
|
|
|
bool compare_exchange_strong(
|
|
std::shared_ptr<T>& expected,
|
|
const std::shared_ptr<T>& desired,
|
|
std::memory_order successOrder = std::memory_order_seq_cst,
|
|
std::memory_order failureOrder = std::memory_order_seq_cst) {
|
|
// See the note at the end of store; we need to defer any destruction we
|
|
// might trigger until after the lock is released.
|
|
// This is not actually needed down the success path (the reference passed
|
|
// in as expected is another pointer to the same object, so we won't
|
|
// decrement the refcount to 0), but "never decrement a refcount while
|
|
// holding a lock" is an easier rule to keep in our heads, and costs us
|
|
// nothing.
|
|
std::shared_ptr<T> prev;
|
|
std::shared_ptr<T> expectedDup;
|
|
{
|
|
std::lock_guard<std::mutex> lg(*detail::atomicReadMostlyMu);
|
|
auto index = curMainPtrIndex_.load(failureOrder);
|
|
ReadMostlyMainPtr<T>& oldMain = mainPtrs_[index];
|
|
if (oldMain.get() != expected.get()) {
|
|
expectedDup = std::move(expected);
|
|
expected = oldMain.getStdShared();
|
|
return false;
|
|
}
|
|
prev = exchangeLocked(desired, successOrder);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
// Must hold the global mutex.
|
|
std::shared_ptr<T> exchangeLocked(
|
|
std::shared_ptr<T> ptr,
|
|
std::memory_order order = std::memory_order_seq_cst) {
|
|
// This is where the tricky bits happen; all modifications of the mainPtrs_
|
|
// and index happen here. We maintain the invariant that, on entry to this
|
|
// method, all read-side critical sections in progress are using the version
|
|
// indicated by curMainPtrIndex_, and the other version is nulled out.
|
|
// (Readers can still hold a ReadMostlySharedPtr to the thing the old
|
|
// version used to point to; they just can't access the old version to get
|
|
// that handle any more).
|
|
auto index = curMainPtrIndex_.load(std::memory_order_relaxed);
|
|
ReadMostlyMainPtr<T>& oldMain = mainPtrs_[index];
|
|
ReadMostlyMainPtr<T>& newMain = mainPtrs_[1 - index];
|
|
DCHECK(newMain.get() == nullptr)
|
|
<< "Invariant should ensure that at most one version is non-null";
|
|
newMain.reset(std::move(ptr));
|
|
// If order is acq_rel, it should degrade to just release, since this is a
|
|
// store rather than an RMW. (Of course, this is such a slow method that we
|
|
// don't really care, but precision is its own reward. If TSAN one day
|
|
// understands asymmetric barriers, this will also improve its error
|
|
// detection here). We get our "acquire-y-ness" from the mutex.
|
|
auto realOrder =
|
|
(order == std::memory_order_acq_rel ? std::memory_order_release
|
|
: order);
|
|
// After this, read-side critical sections can access both versions, but
|
|
// new ones will use newMain.
|
|
// This is also synchronization point with loads.
|
|
curMainPtrIndex_.store(1 - index, realOrder);
|
|
// Wait for all read-side critical sections using oldMain to finish.
|
|
detail::atomicReadMostlyDomain->synchronize();
|
|
// We've reestablished the first half of the invariant (all readers are
|
|
// using newMain), now let's establish the other one (that the other pointer
|
|
// is null).
|
|
auto result = oldMain.getStdShared();
|
|
oldMain.reset();
|
|
return result;
|
|
}
|
|
|
|
// The right way to think of this implementation is as an
|
|
// std::atomic<ReadMostlyMainPtr<T>*>, protected by RCU. There's only two
|
|
// tricky parts:
|
|
// 1. We give ourselves our own RCU domain, and synchronize on modification,
|
|
// so that we don't do any batching of deallocations. This gives
|
|
// shared_ptr-like eager reclamation semantics.
|
|
// 2. Instead of putting the ReadMostlyMainPtrs on the heap, we keep them as
|
|
// part of the same object to improve locality.
|
|
|
|
// Really, just a 0/1 index. This is also the synchronization point for memory
|
|
// orders.
|
|
std::atomic<uint8_t> curMainPtrIndex_;
|
|
|
|
// Both the ReadMostlyMainPtrs themselves and the domain have nontrivial
|
|
// indirections even on the read path, and asymmetric barriers on the write
|
|
// path. Some of these could be fused as a later optimization, at the cost of
|
|
// having to put more tricky threading primitives in this class that are
|
|
// currently abstracted out by those.
|
|
ReadMostlyMainPtr<T> mainPtrs_[2];
|
|
};
|
|
|
|
} // namespace folly
|