I've started the development for the next version
of Boost.Threads recently and have been pondering many of the issues that
people have with the current design. This post is about one of the
problems cited by users on various forums, specifically the need for a
non-scoped locking mechanism. I'm going to lay a lot of ground work for
anyone that doesn't know the history of the problem. If you don't care
about this ground work you can skip ahead to the paragraph that starts
"RFC:".
I still lean towards thinking that it's wrong to
add public lock()/unlock()/etc. methods to the mutex classes because direct
use of such operations is the most common source of error in the code I've had
to deal with. (Granted, the ScopedLock pattern doesn't solve all issues
such as the need to keep the shared data in a consistent state as well as the
lock, but it's a very good step forward and reduces many bugs.) There
are basically two cases where the ScopedLock pattern causes problems for
developers and is why many wish I'd simply add the locking methods to the
mutex classes. Here they are with pseudo-code examples:
1) The need for "overlapping lock
operations", sometimes used for synchronizing "chain" operations such as
linked list traversals.
m1.lock();
do_something_to_m1_shared_data();
m2.lock();
do_something_to_m1_and_m2_shared_data();
m1.unlock();
do_something_to_m2_shared_data();
m2.unlock();
2) The need for locking in one "scope" and
ulocking in another "scope". The best example is in an implementation of
the locking_ptr<> class where a proxy class is used to make
operator-> protect the pointed to object with a mutex, locking it before
access and unlocking it afterwards. Multiple "scopes" occur because the
proxy must be returned by locking_ptr<>::operator-> which
necessitates locking in locking_ptr<>::operator-> (first "scope") and
unlocking in proxy::~proxy() (second "scope"). For a detailed paper on
the technique see http://www.research.att.com/~bs/wrapper.pdf.
The first of these problems is solved by the
ScopedLock concepts in Boost.Threads by including public interfaces for the
lock operations on the ScopedLock instead of on the Mutex. We're still
insured the Mutex will be unlocked but can now easily overlap multiple locks
(as well as other advanced locking schemes).
It's the second problem that's more
difficult. I originally thought I'd solve this solution by creating some
sort of lock_operations<> template that would expose a Mutex's lock
operations externally, thus reducing the likelyhood that a user would abuse
the use of the lock operations solely because they were readily available in
the Mutex's interface. However, this isn't really a great
solution. For one thing it just feels like a hack, and the end result is
still an interface that's easily misused.
Now that I've laid the ground work (a lot of
it... sorry to bore anyone who already knew all of this), here's the crux of
the posting:
RFC:
One night while trying to fall asleep my mind
drifted to this problem (yeah, I know what that says about me), and it dawned
on me that the standard already has a solution to a very similar
problem. The std::auto_ptr<> template solves a similar issue
through "ownership" and "move semantics" instead of the more traditional copy
semantics. This allows you to pass a std::auto_ptr<> out of one
"scope" and into another, passing the ownership and insuring only one "ptr"
will ever delete the object. I could apply the same technique to the
ScopedLock concepts and allow the lock "ownership" to be exclusive and
transferrable. In many ways this seems like a much better solution then
a lock_operations<> template. However, there are issues with "move
semantics" that have caused a lot of questions about how to use
std::auto_ptr<> correctly, and these issues would exist here as
well. Also, I'm not 100% sure this solution will cover all use cases
where the ScopedLock is problematic. So what I'm looking for is comments
on whether or not this solution is viable and/or the solution I should
implement. Thoughts anyone?
Bill Kempf