Boost logo

Boost :

From: Talbot, George (Gtalbot_at_[hidden])
Date: 2008-07-22 18:04:40


Hi all,

A while back (~2006 I think) I was asking around on this list about
using boost::shared_ptr<T> to build a composite data structure that can
be accessed and modified by multiple threads.

Peter Dimov's suggestion at the time was to write a wrapper class for
boost::shared_ptr<T> that would protect access to the
boost::shared_ptr<T> with a spinlock. I've done this, and this works
pretty good in my application. I'm interested in contributing this code
to BOOST, as it's been quite useful.

I have a few questions:

   1) Is there interest in such a donation. It's just one header file,
       and it's wafer-thin...
   2) If so, how does one go about making such a donation?
   3) My implementation below uses a spinlock to protect things and
       as one might expect, under load my application does spend some
       time in ::pthread_spin_lock(). Is there a way to rewrite this
       class (possibly with friendship of boost::shared_ptr<T>) in a
       lock-free manner? I've attached the code below...

Thanks in advance for any help/input you can give.

--
George T. Talbot
<gtalbot_at_[hidden]>
/** \file           atomic_shared.h
 *
 *  \author         George T. Talbot
 *  \author         Peter Dimov
 *
 *  \brief          This file defines a template class that can wrap
 *                  boost::shared_ptr and provide thread-safe atomic
 *                  get/set operations on them.
 *
 *  (C) Copyright George T. Talbot, Peter Dimov 2006.
 *
 *  Distributed under the Boost Software License, Version 1.0. (See
 *  accompanying file LICENSE_1_0.txt or copy at
 *  http://www.boost.org/LICENSE_1_0.txt)
 */
#ifndef ATOMIC_SHARED_H
#define ATOMIC_SHARED_H
#include <pthread.h>
#include <cerrno>
#include <stdexcept>
#include <boost/shared_ptr.hpp>
#include <boost/pointer_cast.hpp>
//#define SPINLOCK_ABORT
#ifdef SPINLOCK_ABORT_ON_ERROR
#define SPINLOCK_ABORT ::abort();
#else
#define SPINLOCK_ABORT
#endif
/** atomic_shared<T> wraps boost::shared_ptr<T> with a spinlock and a
very
 *  restricted set of operations so that a large composite data
structure
 *  may be accessed by multiple threads with a minimum of locking
contention.
 *
 *  Threads working on a large composite data structure create their own
 *  private boost::shared_ptr<T> around their own new nodes and then use
 *  compare_and_set() to place their new nodes in the composite data
 *  structure.
 *
 *  Threads that are viewing the composite data structure may call get()
to
 *  get a private copy of the pointer so they can do some work with it.
The
 *  intended idiom for modifications to nodes is like so:
 *
 *  \code
 *
 *      atomic_shared<const Blah>   global_blah;
 *
 *      void    modify_the_global_blah_in_many_threads_with_no_locking()
 *      {
 *          bool    done = false;
 *          do
 *          {
 *              boost::shared_ptr<const Blah>
my_blah(global_blah.get());
 *
 *              boost::shared_ptr<Blah>         new_blah(*my_blah);
 *              new_blah->some_modification();
 *              done    = global_blah.compare_and_set(my_blah,
new_blah);
 *          }
 *          while (!done);
 *      }
 *
 *  \endcode
 *
 *  Threads that just need to do some work looking at the node:
 *
 *  \code
 *
 *      void    print_the_global_blah_in_many_threads_with_no_locking()
 *      {
 *          boost::shared_ptr<const Blah>   my_blah(global_blah.get());
 *
 *          my_blah->print();
 *      }
 *
 *  \endcode
 *
 *  Using atomic_shared<T> this way satisfies the Thread Safety portion
of
 *  the boost::shared_ptr<T> documentation.
 */
template<class T>
class atomic_shared
{
    /** Initialize a spinlock protecting a shared_ptr<T>. */
    static void init(pthread_spinlock_t& l)
    {
        int rv = ::pthread_spin_init(&l, PTHREAD_PROCESS_PRIVATE);
        
        if (rv)
        {
            SPINLOCK_ABORT
            errno   = rv;
            throw std::runtime_error("can't initialize spinlock");
        }
    }
    /** Used to lock and unlock a spinlock protecting the shared_ptr<>.
     *  Saves typing.
     */
    struct lock_t
    {
        explicit lock_t(pthread_spinlock_t& lock)
            : m_lock(lock)
        {
            if (::pthread_spin_lock(&m_lock))
            {
                SPINLOCK_ABORT
                throw std::runtime_error("can't lock spinlock");
            }
        }
        lock_t(pthread_spinlock_t& lock, int) // Assumes already locked.
            : m_lock(lock)
        {
        }
        ~lock_t()
        {
            if (::pthread_spin_unlock(&m_lock))
            {
                SPINLOCK_ABORT
            }
        }
    private:
        pthread_spinlock_t& m_lock;
    };
    atomic_shared&  operator=(const atomic_shared&);
public:
    typedef boost::shared_ptr<T>            base_ptr;
    /** construct an empty pointer. */
    atomic_shared()
        : ptr(),
          spinlock()
    {
        init(spinlock);
    }
    /** copy constructor.  Since we like to put atomic_shared<> in
     *  std::map<> via
     *  std::map<atomic_shared<T> >::insert(std::make_pair(key, value)),
     *  this needs to be here.
     */
    atomic_shared(const atomic_shared& p)
        : ptr(p.get()),
          spinlock()
    {
        init(spinlock);
    }
    /** construct a protected pointer from a bare one. */
    template<class Y>
    explicit atomic_shared(Y* p)
        : ptr(p),
          spinlock()
    {
        init(spinlock);
    }
    /** construct a protected pointer from a shared_ptr. */
    template<class U>
    explicit atomic_shared(const boost::shared_ptr<U>& p)
        : ptr(p),
          spinlock()
    {
        init(spinlock);
    }
    /** destructor with rudimentary error checking. */
    ~atomic_shared()
    {
        int rv = ::pthread_spin_destroy(&spinlock);
        
        // For debugging, drop us into the debugger if we can't destroy
the
        // spinlock.
        assert(rv == 0);
        (void) rv;
    }
    /** Is it not null? */
    operator bool() const
    {
        bool    rv;
        
        {
            lock_t  l(spinlock);
            rv  = ptr;
        }
        
        return rv;
    }
    /** Is it null? */
    bool    operator!() const
    {
        bool    rv;
        
        {
            lock_t  l(spinlock);
            rv  = !ptr;
        }
        
        return rv;
    }
    /** Right here is the only way to _use_ the pointer.  To use what
the
     *  pointer is pointing to, you must make a private copy in your
thread.
     */
    inline base_ptr get() const
    {
        base_ptr    p;
        
        {
            lock_t    l(spinlock);
            p = ptr;
        }
        
        return p;
    }
    /** Attempt to set the value of the pointer with another pointer,
but
     *  only if the pointer is the same as a previously sampled value of
the
     *  pointer.
     *
     *  @param  cmp     Value of pointer previously retrieved with
get().
     *  @param  x       New value of pointer.
     *  @return true if the pointer could be set to x (i.e. its value
was
     *          still == cmp with the spinlock locked.
     */
    template<class Y, class X>
    bool    compare_and_set(const boost::shared_ptr<Y>& cmp,
                            boost::shared_ptr<X>& x)
    {
        bool    r;
        {
            int rv = ::pthread_spin_trylock(&spinlock);
            switch (rv)
            {
            case EBUSY:
                return false;
            case 0:
                break;
            default:
                SPINLOCK_ABORT
                errno   = rv;
                throw std::logic_error("can't pthread_spin_trylock");
            }
            lock_t    l(spinlock, 0); // already locked
            r = ptr == cmp;
            if (r) ptr = x;
        }
        return r;
    }
private:
    base_ptr                    ptr;
    mutable pthread_spinlock_t  spinlock;
};  // atomic_shared
        
#endif

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