Boost logo

Boost :

From: Kevlin Henney (Kevlin.Henney_at_[hidden])
Date: 2000-01-07 02:06:29


>A challenge:
>
>It sure would be nice if we could write, in C++, a template
> mutexed<T>
>that ensured full mutually exclusion for any arbitrary type T.
>That did not have to know about the methods of T.

Yup, this can be done in a number of ways:

   /1/ An object adaptor (as you have described), where the member
   functions are hidden:

        template<typename T> // where T is of arbitrary type
        class mutexed {...};

   /2/ A variant of class adaptor, using a sandwiched template such that
   the member functions are exposed for direct use.

        template<typename T> // where T is of inheritable class type
        class mutexed : public T {...};

   /3/ A prefix object that is allocated before the object in memory when
   it is created (see the Counted Body Techniques article on
   www.boost.org):

        new(mutex) T

>I was about to start my usual moaning about C++ not really
>allowing transparent proxying - to fully proxy something in C++
>you need to enumerate all of its methods - but I think that we might
>be able to come close.

You can come very close indeed -- in fact you can do it! I originally wrote
a locking proxy (in effect an "execute around pointer") a few years ago as
part of synchronisation framework to do just this, and the principle can be
adapted to your code. It uses a combination of smart pointer and smart
references to achieve the effect, and takes advantage of operator->
chaining.

>Here's a stab at it using some helper macros. (Yeah, well, I hope
>that someone can eliminate the macros.)

Yup :->

>template<typename T>
>class mutexed {
>private:
> lock_t lockvar; // any lock you want - I'll assume just a lock bit
> // handled by primitive processor instructions
> T value;
>public:
> void acquire_lock() { lock.acquire_lock(); }
> void release_lock() { lock.release_lock(); }
>public:
> // constructors don't require locks on constructee
> mutexed() :
> lock(0), // default unlocked
> value() // must have a default constructor;
> // must be constructible without locking
> {}
> mutexed(const mutexed& from) :
> lock(0),
> value()
> {
> from.lock.acquire_lock();
> this->value = from.value;
> from.lock.release_lock();
> // or, use constructor locking, if you want locks released on
exceptions
> }

This should use member initialisation, not assignment, ie

     mutexed(const mutexed& from)
       : lock(0), value((from.acquire_lock(), from.value))
     {
         from.release_lock();
     }

>public:
> // assignment requires locks on both sides

In which case, it would be nice if the mutexes are reentrant, otherwise
deadlock will occur on self assignment.

> T& operator=(mutexed<T>& rhs) {
> assert(this->lock);
> { rhs.lock.acquire_lock();
> this->value = rhs.value;
> rhs.lock.release_lock();
> // or, use constructor locking, if you want locks released on
exceptions
> }
> }

This needs to be made exception safe by using an acquisition object to
acquire and release the lock.

Also, why must the object be prelocked? This seems inconsistent with the
rest of the class. Am I missing something?

>public:
> // destructor - checks lock?
> ~mutexed() {
> assert(lock == 0); // your choice
> }
>public:
> // only allow access via ->

Disagree with this. I looked at such a design and rejected it because it
can lead to excessive locking in a sequence of actions, eg

   p->a();
   p->b();
   p->c();

This locks and unlocks three times and, perhaps more problematically,
allows the sequence to be interleaved with other access. If these three
should be performed together atomically, the class above prevents this
possibility.

There are many different ways of locking objects, and adding locks to
objects (more than I described above), but if this proxied approach is
taken, some consideration must be made for handling multiple actions as a
'transaction'. Either also allow public access to the target members, and
use a conventional acquisition object, or add an "execute around" member
function so that the user passes in a function object that describes the
sequence of actions:

   template<typename UnaryFunction>
   void operator()(UnaryFunction actions)
   {
       lock_acquirer guard(lock);
       actions(value);
   }

> // which checks lock
> // (if I could have -> enforce lock, I'd use that)

See below for solution.

> T* operator->() {
> assert(lock);
> return this;

Should be returning &value.

> }
>};
[...]
>I was hoping that you could do just
>
> mutexed<T> mobj;
> mobj->method(); // implicitly mutexed

This solution can be made more general, but the following code fits with
the code above:

   template<typename T>
   class mutexed_ref
   {
   public:
       mutexed_ref(lock_t & to_lock, T & to_deref)
         : lock(to_lock), target(to_deref), locked(false) {}

       ~mutexed_ref()
       {
           if(locked)
               lock.release_lock();
       }

       T *operator->()
       {
           target->lock();
           locked = true;
           return &target;
       }
   private:
       lock_t & lock;
       T & target;
       bool locked;
   };

This is intended only for use with the reworked mutexed<T>::operator->:

   mutexed_ref<T> operator->()
   {
       return mutexed_ref<T>(lock, value);
   }

Although locking a single member function call is the most typical use, it
clearly does more than this: a full expression is locked. So, caveat
locker: if the object is referred to mutliple times in a single expression
deadlock will occur unless reentrant mutexes are used.

>I'm not quite sure how, but if we could distinguish const mutexed<T>&
>from mutexed<T>& returns, then you could implicitly use multireader
>/ single writer locking. If C++ allowed polymorphism based on
>return type... but it doesn't.

You've now got something you can overload wrt const to get this effect:

   mutexed_ref<T> operator->();
   const mutexed_ref<T> operator->() const;

And then in the ref class you can again overload const to perform either a
read or write lock on the synchronisation primitive.

Kevlin


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk