Boost logo

Boost :

From: Howard Hinnant (hinnant_at_[hidden])
Date: 2004-07-06 15:30:41


I like the latent_write_lock / upgradeable lock idea proposed by Bruno
/ Alexander. I have been playing with the following interface:

namespace detail
{

template <class RW_Mutex>
class read_lock
{
public:
     typedef RW_Mutex mutex_type;

     explicit read_lock(mutex_type& m, bool lock_it = true);
     explicit read_lock(upgradable_read_lock<mutex_type>& r);
     explicit read_lock(write_lock<mutex_type>& w);

     ~read_lock();

     void lock();
     bool try_lock();
     void unlock();
     bool locked() const;
     operator int bool_type::* () const;

private:
     read_lock(const read_lock&);
     read_lock& operator=(const read_lock&);
};

template <class RW_Mutex>
class upgradable_read_lock
{
public:
     typedef RW_Mutex mutex_type;

     explicit upgradable_read_lock(mutex_type& m, bool lock_it = true);
     explicit upgradable_read_lock(write_lock<mutex_type>& w);

     ~upgradable_read_lock();

     void lock();
     bool try_lock();
     void unlock();
     bool locked() const;
     operator int bool_type::* () const;

     void transfer_to(read_lock<mutex_type>& r);
     void transfer_to(write_lock<mutex_type>& w);

private:
     upgradable_read_lock(const upgradable_read_lock&);
     upgradable_read_lock& operator=(const upgradable_read_lock&);
};

template <class RW_Mutex>
class write_lock
{
public:
     typedef RW_Mutex mutex_type;

     explicit write_lock(mutex_type& m, bool lock_it = true);
     explicit write_lock(upgradable_read_lock<mutex_type>& r);

     ~write_lock();

     void lock();
     bool try_lock();
     void unlock();
     bool locked() const;
     operator int bool_type::* () const;

     void transfer_to(read_lock<mutex_type>& r);
     void transfer_to(upgradable_read_lock<mutex_type>& r);

private:
     write_lock(const write_lock&);
     write_lock& operator=(const write_lock&);
};

} // detail

class rw_mutex
{
public:
     typedef detail::read_lock<rw_mutex> read_lock;
     typedef detail::upgradable_read_lock<rw_mutex>
upgradable_read_lock;
     typedef detail::write_lock<rw_mutex> write_lock;

     rw_mutex();
     ~rw_mutex();

private:
     rw_mutex(const rw_mutex&);
     rw_mutex& operator=(const rw_mutex&);
};

There are 3 types of locks: read_lock, upgradable_read_lock and
write_lock. Conversions (transfer of mutex ownership) between these
types will only occur in an atomic fashion (blocking when appropriate).
  Conversions that might result in a non-atomic operation are not
allowed. Conversions take two forms:

1. Conversion constructor.
2. Member function: void transfer_to(...);

The conversion ctor locks only if its argument was locked. The ctor
argument is unlocked after the construction.

The transfer_to function will throw a lock_error if the lock was not
locked, or if the argument does not refer to the same underlying mutex.
  When s.transfer_to(d) returns (normally), s is unlocked and d is
locked (atomically).

Destructors unlock only themselves, and have no effect on other locks
(there is no persistent linking implied by mutex ownership transfer).
If such a link is desired, it is easily built on top of this framework.

As described by others, read_locks can share ownership among themselves
and not more than one upgradable_read_lock. write_locks maintain
exclusive ownership.

Here is how some recent code examples might look:

     void f(rw_mutex& m)
     {
         rw_mutex::upgradable_read_lock r(m);
         if (...)
         {
             rw_mutex::write_lock w(r); //lock promotion
             //...
             w.transfer_to(r);
         }
         //Point A
     }

If you wanted to catch exceptions at Point A thrown while writing, and
still have the read_lock, a simple auxiliary RAII class could be
employed:

template <class Lock1, class Lock2>
class helper
{
public:
     explicit helper(Lock2& l2) : l1_(l2), l2_(l2) {}
     ~helper() {l1_.transfer_to(l2_);}
private:
     helper(const helper&);
     helper& operator=(const helper&);

     Lock1 l1_;
     Lock2& l2_;
};

and then f would look like:

     void f(rw_mutex& m)
     {
         rw_mutex::upgradable_read_lock r(m);
         if (...)
         {
             helper<rw_mutex::write_lock,
rw_mutex::upgradable_read_lock> w(r);
             //...
         }
         //Point A
     }

Note that if you don't catch exceptions at Point A, the helper class is
needless complexity and transfer of the mutex. It would also be a
needless transfer if there is no code at Point A, or if the write
section returns early. That is why I like the locks to be unlinked.
Linking them would add space overhead to the locks, and time overhead
to the destructors, when it is not always desired. But linking is
easily built on top with a helper class as shown (only pay for what you
use).

Another example:

      void f(rw_mutex& m)
      {
          rw_mutex::write_lock w(m);
          if (...)
          {
              rw_mutex::upgradable_read_lock r(w); //lock demotion
              //...
              r.transfer_to(w);
          }
          //Point A
      }

Same notes about exceptions and helpers.

If one accidently codes read_lock instead of upgradable_read_lock, then
the transfer_to() call will fail at compile time. If the
r.tranfer_to(w) is not needed however, then a read_lock will work just
fine.

The downside to all this is that the sizeof(rw_mutex) about doubled to
support upgradable_read_lock (whether or not you use that
functionality). The code size of the read and write locking/unlocking
also swelled somewhat. I'm still contemplating the cost/functionality
tradeoff, but my current feeling is that it is probably worth it.

-Howard


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