Blag

He's not dead, he's resting

Tag Archives: threads

Implementing Active Objects using Smart Pointers

An Active Object is, essentially, a threaded design pattern where an object is always called from the same thread, and where a proxy provides synchronised access to that object. It’s useful in cases where there’s little or no parallelism possible due to the underlying object’s state, and where implementing manual locking would be a nuisance.

The problem in C++, generally, is implementing the proxy. If the underlying object’s class has twenty methods, you have to implement twenty trivial wrapper methods for the proxy that obtain the lock and then forward. Whilst not difficult to do, it’s rather tedious.

But what if the proxy is a smart pointer? Then you could do proxy->method() for any method the underlying class has, and you wouldn’t have to worry about writing wrapper methods. The question then is how to write Proxy<UnderlyingClass>::operator-> ().

It can’t simply return the underlying instance, since it needs to do locking. And it can’t obtain a lock and then return the underlying instance, since the lock would never be released.

But all is not lost. The standard has some rather interesting wording:

An expression x->m is interpreted as (x.operator->())->m for a class object x of type T if T::operator-> () exists and if the operator is selected as the best match function by the overload resolution mechanism.

Usually, implementations of operator-> () simply return SomeType *. But with the way the standard is worded, they could instead return a second class instance, so long as that new class has operator-> () defined.

So we make Proxy<UnderlyingClass>::operator-> () return a temporary that, upon construction, obtains the shared lock, and upon destruction releases it. Then we rely upon the temporary’s operator-> () returning a pointer to the underlying object to get the actual method call.

Except… It’s not that simple. With current C++, the temporary has to be copyable (even though the compiler optimises out the copy), but typically mutex locks are noncopyable. With C++0x we’ll be able to return by rvalue reference, but until then we have to store the lock in a shared pointer rather than directly.

We’re going to start making use of this in Paludis to cut down on boilerplate code. A working implementation follows. We have a couple of refinements: the class is called ActiveObjectPtr, and it’s parameterised by a pointer to the underlying class (we’ll see why in a later post — generally it’ll just be a std::tr1::shared_ptr, but leaving it as a parameter lets us compose smart pointer types).

template <typename T_>
class ActiveObjectPtr
{
    private:
        T_ _ptr;
        std::tr1::shared_ptr<Mutex> _mutex;

        class Deref
        {
            private:
                const ActiveObjectPtr * _ptr;
                std::tr1::shared_ptr<Lock> _lock;

            public:
                Deref(const ActiveObjectPtr * p) :
                    _ptr(p),
                    _lock(make_shared_ptr(new Lock(*p->_mutex)))
                {
                }

                const T_ & operator-> () const
                {
                    return _ptr->_ptr;
                }
        };

        friend class Deref;

    public:
        ActiveObjectPtr(const T_ & t) :
            _ptr(t),
            _mutex(new Mutex)
        {
        }

        ActiveObjectPtr(const ActiveObjectPtr & other) :
            _ptr(other._ptr),
            _mutex(other._mutex)
        {
        }

        ~ActiveObjectPtr()
        {
        }

        ActiveObjectPtr &
        operator= (const ActiveObjectPtr & other)
        {
            if (this != &other)
            {
                _ptr = other._ptr;
                _mutex = other._mutex;
            }
            return *this;
        }

        Deref operator-> () const
        {
            return Deref(this);
        }
};

At this stage, it’s not really a proper smart pointer. It doesn’t (and probably shouldn’t) have operator*, and it ignores the const issue. Handling these is left as an easy exercise for the reader.