sapling/eden/fs/utils/Rcu.h
Xavier Deguillard 88dcfa7bb3 utils: add an RcuPtr class
Summary:
RCU is a synchronization mechanism that allows for very fast reads, at the
expense of slower writes. This is achieved by having the reader sometimes
reading a stale pointer when concurrent to a write, at which point the writer
will delay reclaiming the old data to a later moment where it is known that no
reader can hold a pointer to the old data.

Doing that allows for the read operations to be significantly faster than using
a Synchronized lock. Folly's documentation claims a read lock/unlock of RCU
runs in ~4ns, while the same for Synchronized is ~26ns.

Due to the writers cost, RCU is perfectly suited for places where reads needs
to be as fast as possible, and writes are very infrequent. One typical example
is when caching an application's configuration, we can expect reading the
configuration values more frequently than it is being reloaded, and in the case
where the configuration mismatch, a stale configuration can be tolerated by the
application.

In EdenFS, we can use RCU on Windows to make sure that unmounting a repository
will wait on all the pending callbacks.

Reviewed By: kmancini

Differential Revision: D25351536

fbshipit-source-id: 050ca0337e67ae195f4f16062dddb60f584af692
2020-12-11 14:10:58 -08:00

162 lines
3.9 KiB
C++

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This software may be used and distributed according to the terms of the
* GNU General Public License version 2.
*/
#pragma once
#include <folly/synchronization/Rcu.h>
namespace facebook::eden {
/**
* Smart pointer to automatically manage RCU resources.
*
* For details about RCU: https://en.wikipedia.org/wiki/Read-copy-update
*/
template <
typename T,
typename RcuTag = folly::RcuTag,
typename Deleter = std::default_delete<T>>
class RcuPtr {
public:
using RcuDomain = folly::rcu_domain<RcuTag>;
/**
* Smart pointer that ensures the proper use of the rcu_reader guard.
*
* The managed resource is guaranteed to be valid as long as this object is
* alive. It is expected that an RcuLockedPtr is short lived, as live
* RcuLockedPtr would prevent the RCU domain to synchronize, potentially
* leading to memory from other RcuPtr from being reclaimed.
*/
class RcuLockedPtr {
public:
~RcuLockedPtr() = default;
RcuLockedPtr(const RcuLockedPtr&) = delete;
RcuLockedPtr& operator=(const RcuLockedPtr&) = delete;
RcuLockedPtr(RcuLockedPtr&& other) = default;
RcuLockedPtr& operator=(RcuLockedPtr&&) = default;
/**
* Return a pointer to the inner resource.
*
* The lifetime of the returned value is the same as the RcuLockedPtr.
*/
T* get() const noexcept {
return inner_;
}
T& operator*() const noexcept {
return *inner_;
}
T* operator->() const noexcept {
return inner_;
}
explicit operator bool() const noexcept {
return inner_;
}
private:
friend RcuPtr<T, RcuTag, Deleter>;
/**
* Construct the smart pointer.
*
* The RCU section needs to be created first to ensure that the pointer
* isn't dangling.
*/
explicit RcuLockedPtr(RcuPtr& self)
: guard_(&self.domain_),
inner_(self.inner_.load(std::memory_order_acquire)) {}
folly::rcu_reader_domain<RcuTag> guard_;
T* inner_;
};
template <class... Args>
explicit RcuPtr(RcuDomain& rcuDomain, Args&&... args)
: domain_(rcuDomain), inner_(new T(std::forward<Args>(args)...)) {}
RcuPtr(const RcuPtr&) = delete;
RcuPtr& operator=(const RcuPtr&) = delete;
RcuPtr(RcuPtr&&) = delete;
RcuPtr& operator=(RcuPtr&&) = delete;
/**
* Destroy this RcuPtr.
*
* The underlying resource will be asynchronously freed.
*/
~RcuPtr() {
reset();
}
void reset() {
update_inner(nullptr);
}
/**
* Obtain a reference to the inner resource.
*/
RcuLockedPtr rlock() noexcept {
return RcuLockedPtr{*this};
}
/**
* Build a new resource in place.
*
* Returns the old resource. As concurrent threads may be holding a
* RcuLockedPtr with the returned pointer, care must be taken to not free it
* until they all RcuLockedPtr are destroyed. The use of RcuPtr::synchronize
* after this can be used to that effect.
*/
template <class... Args>
T* exchange(Args&&... args) noexcept {
return exchange_inner(new T(std::forward<Args>(args)...));
}
/**
* Swap the inner resource and release it.
*
* The resource is freed asynchronously.
*/
template <class... Args>
void update(Args&&... args) noexcept {
update_inner(new T(std::forward<Args>(args)...));
}
/**
* Blocks until no RcuLockedPtr are live.
*
* This only waits until all the live RcuLockedPtr at the time the function
* is called are destroyed.
*/
void synchronize() noexcept {
domain_.synchronize();
}
private:
friend class RcuPtr<T, RcuTag>::RcuLockedPtr;
T* exchange_inner(T* inner) {
return inner_.exchange(inner, std::memory_order_acq_rel);
}
void update_inner(T* inner) {
auto old = exchange_inner(inner);
domain_.call([old] { Deleter()(old); });
}
RcuDomain& domain_;
std::atomic<T*> inner_{};
};
} // namespace facebook::eden