ardour/libs/pbd/pbd/rcu.h

341 lines
11 KiB
C++

/*
* Copyright (C) 2000-2013 Paul Davis <paul@linuxaudiosystems.com>
* Copyright (C) 2009 David Robillard <d@drobilla.net>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef __pbd_rcu_h__
#define __pbd_rcu_h__
#include <atomic>
#include <mutex>
#include <memory>
#include "boost/smart_ptr/detail/yield_k.hpp"
#include <list>
#include "pbd/libpbd_visibility.h"
/** @file rcu.h
* Define a set of classes to implement Read-Copy-Update. We do not attempt to define RCU here - use google.
*
* The design consists of two parts: an RCUManager and an RCUWriter.
*/
/** An RCUManager is an object which takes over management of a pointer to another object.
*
* It provides three key methods:
*
* - reader() : obtains a shared pointer to the managed object that may be used for reading, without synchronization
* - write_copy() : obtains a shared pointer to the object that may be used for writing/modification
* - update() : accepts a shared pointer to a (presumed) modified instance of the object and causes all
* future reader() and write_copy() calls to use that instance.
*
* Any existing users of the value returned by reader() can continue to use their copy even as a write_copy()/update() takes place.
* The RCU manager will manage the various instances of "the managed object" in a way that is transparent to users of the manager
* and managed object.
*/
template <class T>
class /*LIBPBD_API*/ RCUManager
{
public:
RCUManager (T* object_to_be_managed)
{
_active_reads = 0;
managed_object = new std::shared_ptr<T> (object_to_be_managed);
}
virtual ~RCUManager ()
{
/* This just deletes the shared ptr, but of course this may
also be the last reference to the managed object.
*/
delete managed_object.load ();
}
std::shared_ptr<T const> reader () const
{
std::shared_ptr<T> rv;
/* Keep count of any readers in this section of code, so writers can
* wait until managed_object is no longer in use after an atomic exchange
* before dropping it.
*/
/* no reads or writes below this atomic store (e.g. the copying
* of *managed_object) can move before this "barrier".
*/
_active_reads.fetch_add (1, std::memory_order_release);
rv = *managed_object;
/* no reads or writes below this atomic store (e.g. the copying
* of *managed_object) can move before this "barrier", and this
* also synchronizes with a memory_order_acquire load when
* testing for active readers (see below).
*/
_active_reads.fetch_sub (1, std::memory_order_release);
return rv;
}
/* this is an abstract base class - how these are implemented depends on the assumptions
* that one can make about the users of the RCUManager. See SerializedRCUManager below
* for one implementation.
*/
virtual std::shared_ptr<T> write_copy () = 0;
virtual bool update (std::shared_ptr<T> new_value) = 0;
protected:
typedef std::shared_ptr<T>* PtrToSharedPtr;
std::atomic<PtrToSharedPtr> managed_object;
inline bool active_read () const {
return _active_reads.load (std::memory_order_acquire) != 0;
}
private:
mutable std::atomic<int> _active_reads;
};
/** Serialized RCUManager implements the RCUManager interface. It is based on the
* following key assumption: among its users we have readers that are bound by
* RT time constraints, and writers who are not. Therefore, we do not care how
* slow the write_copy()/update() operations are, or what synchronization
* primitives they use.
*
* Because of this design assumption, this class will serialize all
* writers. That is, objects calling write_copy()/update() will be serialized by
* a mutex. Only a single writer may be in the middle of write_copy()/update();
* all other writers will block until the first has finished. The order of
* execution of multiple writers if more than one is blocked in this way is
* undefined.
*
* The class maintains a lock-protected "dead wood" list of old value of
* *managed_object (i.e. shared_ptr<T>). The list is cleaned up every time we call
* write_copy(). If the list is the last instance of a shared_ptr<T> that
* references the object (determined by shared_ptr::unique()) then we
* erase it from the list, thus deleting the object it points to. This is lazy
* destruction - the SerializedRCUManager assumes that there will sufficient
* calls to write_copy() to ensure that we do not inadvertently leave objects
* around for excessive periods of time.
*
* For extremely well defined circumstances (i.e. it is known that there are no
* other writer objects in existence), SerializedRCUManager also provides a
* flush() method that will unconditionally clear out the "dead wood" list. It
* must be used with significant caution, although the use of shared_ptr<T>
* means that no actual objects will be deleted incorrectly if this is misused.
*/
template <class T>
class /*LIBPBD_API*/ SerializedRCUManager : public RCUManager<T>
{
public:
SerializedRCUManager(T* new_managed_object)
: RCUManager<T>(new_managed_object)
, _current_write_old (0)
{
}
void init (std::shared_ptr<T> object_to_be_managed) {
assert (*RCUManager<T>::managed_object == std::shared_ptr<T> ());
RCUManager<T>::managed_object = new std::shared_ptr<T> (object_to_be_managed);
}
std::shared_ptr<T> write_copy ()
{
_lock.lock ();
// clean out any dead wood
typename std::list<std::shared_ptr<T> >::iterator i;
for (i = _dead_wood.begin (); i != _dead_wood.end ();) {
if ((*i).unique ()) {
i = _dead_wood.erase (i);
} else {
++i;
}
}
/* store the current so that we can do compare and exchange
* when someone calls update(). Notice that we hold
* a lock, so this store of managed_object is atomic.
*/
_current_write_old = RCUManager<T>::managed_object;
/* now do the (potentially arbitrarily expensive data copy of
* the RCU-managed object
*/
std::shared_ptr<T> new_copy (new T (**_current_write_old));
return new_copy;
/* notice that the write lock is still held: update() or abort() MUST
* be called or we will cause another writer to stall.
*/
}
void abort () {
_lock.unlock();
}
bool update (std::shared_ptr<T> new_value)
{
/* we still hold the write lock - other writers are locked out */
typename RCUManager<T>::PtrToSharedPtr new_spp = new std::shared_ptr<T> (new_value);
/* update, by atomic compare&swap. Only succeeds if the old
* value has not been changed.
*
* XXX but how could it? we hold the freakin' lock!
*/
bool ret = RCUManager<T>::managed_object.compare_exchange_strong (_current_write_old, new_spp);
if (ret) {
/* successful update
*
* wait until there are no active readers. This ensures that any
* references to the old value have been fully copied into a new
* shared_ptr, and thus have had their reference count incremented.
*/
for (unsigned i = 0; RCUManager<T>::active_read (); ++i) {
/* spin being nice to the scheduler/CPU */
boost::detail::yield (i);
}
#if 0 // TODO find a good solition here...
/* if we are not the only user, put the old value into dead_wood.
* if we are the only user, then it is safe to drop it here.
*/
if (!_current_write_old->unique ()) {
_dead_wood.push_back (*_current_write_old);
}
#else
/* above ->unique() condition is subject to a race condition.
*
* Particulalry with JACK2 graph-order callbacks arriving
* concurrently to processing, which can lead to heap-use-after-free
* of the RouteList.
*
* std::shared_ptr<T>::use_count documetation reads:
* > In multithreaded environment, the value returned by use_count is approximate
* > (typical implementations use a memory_order_relaxed load).
*/
_dead_wood.push_back (*_current_write_old);
#endif
/* now delete it - if we are the only user, this deletes the
* underlying object. If other users existed, then there will
* be an extra reference in _dead_wood, ensuring that the
* underlying object lives on even when the other users
* are done with it
*/
delete _current_write_old;
}
/* unlock, allowing other writers to proceed */
_lock.unlock ();
return ret;
}
void no_update () {
/* just releases the lock, in the event that no changes are
made to a write copy.
*/
_lock.unlock ();
}
void flush ()
{
std::lock_guard<std::mutex> lm (_lock);
_dead_wood.clear ();
}
private:
std::mutex _lock;
typename RCUManager<T>::PtrToSharedPtr _current_write_old;
std::list<std::shared_ptr<T> > _dead_wood;
};
/** RCUWriter is a convenience object that implements write_copy/update via
* lifetime management. Creating the object obtains a writable copy, which can
* be obtained via the get_copy() method; deleting the object will update
* the manager's copy. Code doing a write/update thus looks like:
*
* @code
* {
* RCUWriter writer (object_manager);
* std::shared_ptr<T> copy = writer.get_copy();
* ... modify copy ...
*
* } <= writer goes out of scope, update invoked
* @endcode
*
*/
template <class T>
class /*LIBPBD_API*/ RCUWriter
{
public:
RCUWriter (RCUManager<T>& manager)
: _manager (manager)
, _copy (_manager.write_copy ())
{
}
~RCUWriter ()
{
if (_copy.unique ()) {
/* As intended, our copy is the only reference
to the object pointed to by _copy. Update
the manager with the (presumed) modified
version.
*/
_manager.update (_copy);
} else {
/* This means that some other object is using our copy
* of the object. This can only happen if the scope in
* which this RCUWriter exists passed it to a function
* that created a persistent reference to it, since the
* copy was private to this particular RCUWriter. Doing
* so will not actually break anything but it violates
* the design intention here and so we do not bother to
* update the manager's copy.
*
* XXX should we print a warning about this?
*/
}
}
std::shared_ptr<T> get_copy () const
{
return _copy;
}
private:
RCUManager<T>& _manager;
std::shared_ptr<T> _copy;
};
#endif /* __pbd_rcu_h__ */