1
0
mirror of https://github.com/Zygo/bees.git synced 2025-05-17 13:25:45 +02:00
bees/include/crucible/namedptr.h
Zygo Blaxell 21c08008e6 namedptr: add some doxygen, fix the #endif comment
Document the overall purpose of the class and what some of the methods do,
particularly the ones with terrible names like 'insert_item' (which only
inserts an item after calling the Function).

Signed-off-by: Zygo Blaxell <bees@furryterror.org>
2022-12-20 20:50:54 -05:00

226 lines
7.5 KiB
C++

#ifndef CRUCIBLE_NAMEDPTR_H
#define CRUCIBLE_NAMEDPTR_H
#include "crucible/lockset.h"
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <tuple>
namespace crucible {
using namespace std;
/// A thread-safe container for RAII of shared resources with unique names.
template <class Return, class... Arguments>
class NamedPtr {
public:
/// The name in "NamedPtr"
using Key = tuple<Arguments...>;
/// A shared pointer to the named object with ownership
/// tracking that erases the object's stored name when
/// the last shared pointer is destroyed.
using Ptr = shared_ptr<Return>;
/// A function that translates a name into a shared pointer to an object.
using Func = function<Ptr(Arguments...)>;
private:
struct Value;
using WeakPtr = weak_ptr<Value>;
using MapType = map<Key, WeakPtr>;
struct MapRep {
MapType m_map;
mutex m_mutex;
};
using MapPtr = shared_ptr<MapRep>;
/// Container for Return pointers. Destructor removes entry from map.
struct Value {
Ptr m_ret_ptr;
MapPtr m_map_rep;
Key m_ret_key;
~Value();
Value(Ptr&& ret_ptr, const Key &key, const MapPtr &map_rep);
};
Func m_fn;
MapPtr m_map_rep = make_shared<MapRep>();
LockSet<Key> m_lockset;
Ptr lookup_item(const Key &k);
Ptr insert_item(Func fn, Arguments... args);
public:
NamedPtr(Func f = Func());
void func(Func f);
Ptr operator()(Arguments... args);
Ptr insert(const Ptr &r, Arguments... args);
};
/// Construct NamedPtr map and define a function to turn a name into a pointer.
template <class Return, class... Arguments>
NamedPtr<Return, Arguments...>::NamedPtr(Func f) :
m_fn(f)
{
}
/// Construct a Value wrapper: the value to store, the argument key to store the value under,
/// and a pointer to the map. Everything needed to remove the key from the map when the
/// last NamedPtr is deleted. NamedPtr then releases its own pointer to the value, which
/// may or may not trigger deletion there.
template <class Return, class... Arguments>
NamedPtr<Return, Arguments...>::Value::Value(Ptr&& ret_ptr, const Key &key, const MapPtr &map_rep) :
m_ret_ptr(ret_ptr),
m_map_rep(map_rep),
m_ret_key(key)
{
}
/// Destroy a Value wrapper: remove a dead Key from the map, then let the member destructors
/// do the rest. The Key might be in the map and not dead, so leave it alone in that case.
template <class Return, class... Arguments>
NamedPtr<Return, Arguments...>::Value::~Value()
{
unique_lock<mutex> lock(m_map_rep->m_mutex);
// We are called from the shared_ptr destructor, so we
// know that the weak_ptr in the map has already expired;
// however, if another thread already noticed that the
// map entry expired while we were waiting for the lock,
// the other thread will have already replaced the map
// entry with a pointer to some other object, and that
// object now owns the map entry. So we do a key lookup
// here instead of storing a map iterator, and only erase
// "our" map entry if it exists and is expired. The other
// thread would have done the same for us if the race had
// a different winner.
const auto found = m_map_rep->m_map.find(m_ret_key);
if (found != m_map_rep->m_map.end() && found->second.expired()) {
m_map_rep->m_map.erase(found);
}
}
/// Find a Return by key and fetch a strong Return pointer.
/// Ignore Keys that have expired weak pointers.
template <class Return, class... Arguments>
typename NamedPtr<Return, Arguments...>::Ptr
NamedPtr<Return, Arguments...>::lookup_item(const Key &k)
{
// Must be called with lock held
const auto found = m_map_rep->m_map.find(k);
if (found != m_map_rep->m_map.end()) {
// Get the strong pointer back
const auto rv = found->second.lock();
if (rv) {
// Have strong pointer. Return value that shares map entry.
return shared_ptr<Return>(rv, rv->m_ret_ptr.get());
}
// Have expired weak pointer. Another thread is trying to delete it,
// but we got the lock first. Leave the map entry alone here.
// The other thread will erase it, or we will put a different entry
// in the same map entry.
}
return Ptr();
}
/// Insert the Return value of calling Func(Arguments...).
/// If the value already exists in the map, return the existing value.
/// If another thread is already running Func(Arguments...) then this thread
/// will block until the other thread finishes inserting the Return in the
/// map, and both threads will return the same Return value.
template <class Return, class... Arguments>
typename NamedPtr<Return, Arguments...>::Ptr
NamedPtr<Return, Arguments...>::insert_item(Func fn, Arguments... args)
{
Key k(args...);
// Is it already in the map?
unique_lock<mutex> lock_lookup(m_map_rep->m_mutex);
auto rv = lookup_item(k);
if (rv) {
return rv;
}
// Release map lock and acquire key lock
lock_lookup.unlock();
const auto key_lock = m_lockset.make_lock(k);
// Did item appear in map while we were waiting for key?
lock_lookup.lock();
rv = lookup_item(k);
if (rv) {
return rv;
}
// We now hold key and index locks, but item not in map (or expired).
// Release map lock so other threads can use the map
lock_lookup.unlock();
// Call the function and create a new Value outside of the map
const auto new_value_ptr = make_shared<Value>(fn(args...), k, m_map_rep);
// Function must return a non-null pointer
THROW_CHECK0(runtime_error, new_value_ptr->m_ret_ptr);
// Reacquire index lock for map insertion. We still hold the key lock.
// Use a different lock object to make exceptions unlock in the right order
unique_lock<mutex> lock_insert(m_map_rep->m_mutex);
// Insert return value in map or overwrite existing
// empty or expired weak_ptr value.
WeakPtr &new_item_ref = m_map_rep->m_map[k];
// We searched the map while holding both locks and
// found no entry or an expired weak_ptr; therefore, no
// other thread could have inserted a new non-expired
// weak_ptr, and the weak_ptr in the map is expired
// or was default-constructed as a nullptr. So if the
// new_item_ref is not expired, we have a bug we need
// to find and fix.
assert(new_item_ref.expired());
// Update the map slot we are sure is empty
new_item_ref = new_value_ptr;
// Return shared_ptr to Return using strong pointer's reference counter
return shared_ptr<Return>(new_value_ptr, new_value_ptr->m_ret_ptr.get());
// Release map lock, then key lock
}
/// (Re)define a function to turn a name into a pointer.
template <class Return, class... Arguments>
void
NamedPtr<Return, Arguments...>::func(Func func)
{
unique_lock<mutex> lock(m_map_rep->m_mutex);
m_fn = func;
}
/// Convert a name into a pointer using the configured function.
template<class Return, class... Arguments>
typename NamedPtr<Return, Arguments...>::Ptr
NamedPtr<Return, Arguments...>::operator()(Arguments... args)
{
return insert_item(m_fn, args...);
}
/// Insert a pointer that has already been created under the
/// given name. Useful for inserting a pointer to a derived
/// class when the name doesn't contain all of the information
/// required for the object, or when the Return is already known by
/// some cheaper method than calling the function.
template<class Return, class... Arguments>
typename NamedPtr<Return, Arguments...>::Ptr
NamedPtr<Return, Arguments...>::insert(const Ptr &r, Arguments... args)
{
THROW_CHECK0(invalid_argument, r);
return insert_item([&](Arguments...) { return r; }, args...);
}
}
#endif // CRUCIBLE_NAMEDPTR_H