Boost logo

Threads-Devel :

From: Anthony Williams (anthony_at_[hidden])
Date: 2006-03-01 08:58:58


Hi all,

I thought I'd start off some discussion about mutexes, to inaugurate our new
mailing list.

The mutex is the basic synchronization primitive for shared-memory threading
models, such as that of boost.thread, so it is very important that the model,
and implementation, is sound. With a working mutex, you can build more
expressive synchronization mechanisms, such as the smart shared pointers
proposed by Roland.

In C++, there are several primary usage scenarious for mutex objects:

1. Standalone, as an object of static storage duration at namespace or block
scope.

2. As a class member, for a class object of static storage duration.

3. As a class member, for a class object created on the stack or heap.

In each case, the mutex object is used to synchronize access to other objects
at the same scope.

It is essential that the mutex be correctly initialized by the time the first
thread attempts to lock it. For objects of static storage duration, this
requires that the mutex can be initialized with a static initializer, and not
rely on dynamic initialization, since dynamic initialization exposes the
possibility of race conditions.

Constructor calls constitute dynamic initialization, so therefore we need
mutex objects which are aggregates, with no members that require constructor
calls, so there are no constructor calls. Ideally, zero-initialization would
be sufficient, as then the user would not have to specify an initializer, but
failing that, a static initializer akin to PTHREAD_MUTEX_INITIALIZER would
work. e.g.

boost::mutex m=BOOST_MUTEX_INITIALIZER;

At the other end of the application lifetime, we have destruction. Since we
have no idea how many threads will be running during application shutdown, or
what order objects will be destructed, we need to be conservative here, too
--- we need the mutex objects to remain usable for as long as possible. On the
platforms which I know about, it is acceptable to leak OS resources such as
mutexes, as these will be cleaned up by the OS on program exit.

Therefore, I suggest that the most straightforward way to deal with
destruction order race conditions is for the mutex objects to have no
destructors (which is consistent with being POD).

For the second use case above, we have a slightly different scenario. As a
class member, we cannot use a static initializer, unless the enclosing class
is also an aggregate. Therefore, we need to rely on zero initialization, or a
constructor call.

The constructor call in question could be the copy constructor, if we provide
a function that returns a suitably-initialized object to copy, or we provide a
statically-initialized "master copy". However, in either case the user then
has to remember to explicitly initialize the member. e.g.

class X
{
    boost::mutex m;
public:
    X():
        m(BOOST_MUTEX_DYNAMIC_INITIALIZER)
    {}
};

X x;

If the user has an object of static storage duration which requires dynamic
initialization, then it has to be up to them to ensure that the aforementioned
possibilities of race conditions are dealt with. Also, once we're in to
dynamic initialization, then this case is really akin to the third case.

In the third case, as a member of an object on the stack or heap, we cannot
rely on zero initialization, so some form of initializer is a must. The
preference would be for a default constructor, but this clashes with the
requirement for pure-static initialization above. If we can rely on
zero-initialization, just requiring explicit initialization with an empty
initializer is enough, but that's easy to miss. The dynamic initializer
suggested above would work, as would an explicit initialize() function, but
both of these are cumbersome.

In this case, destruction is actually now an important issue --- if the object
does not live for the length of the whole application, then leaking OS
resources is not acceptable. We could provide an explicit destroy() function,
akin to pthread_mutex_destroy(), but in C++ this is really the territory of
destructors.

I therefore suggest that we supply two mutex types, one of which is POD, and
requires a static initializer, and one of which has a default constructor and
a destructor. Of course, if we equip the first one with explicit initialize()
and destroy() functions, then the second can just be a wrapper for the first
that calls initialize() in the constructor, and destroy() in the destructor.

I suggest that these two different variants are named boost::static_mutex and
boost::mutex.

This suggestion is orthogonal to any other variation in the mutex type, so we
would also have boost::static_try_mutex vs boost::try_mutex, and so forth.

I have implemented my suggestion for Win32, except at the moment the types are
named boost::detail::basic_mutex and boost::mutex. I have also implemented
boost::detail::basic_recursive_mutex and
boost::recursive_mutex. boost::try_mutex and boost::recursive_try_mutex are
also implemented, as simple extensions, since the basic_xxx variants support
try_locks. These are all in the boost CVS, on the thread-rewrite branch. The
reason I have left the basic_xxx classes in boost::detail is because they are
just primitives; they expose the lock/unlock functionality directly, rather
than through the scoped_lock classes dictated by the current interface docs.

When I last looked at this, I named the basic_mutex version lightweight_mutex,
and had a discussion about it with Roland. The earlier version is still in
CVS, but is no longer used for boost::mutex. I believe the current version is
marginally simpler, and also boost::detail::lightweight_mutex is a name used
by the shared_ptr implementation, which is why I have changed it to
basic_mutex. The old implementation has been left in, as I haven't yet changed
my implementation of read_write_mutex, which uses it.

One of Roland's concerns was that this essentially duplicates CRITICAL_SECTION
functionality. The lightweight_mutex used with shared_ptr is essentially just
a wrapper around CRITICAL_SECTION on Windows, so why do we need anything
different?

The answer to this is use case 1 above --- CRITICAL_SECTION objects don't have
static initializers; they require a call to InitializeCriticalSection, or
InitializeCriticalSectionAndSpinCount before use.

I could have just used a Win32 Mutex object, but that's quite
heavyweight. Instead, the code uses a Win32 Semaphore, alongside a lock
count. The semaphore is only used if there is contention for the lock, so if
contention is rare there is a performance boost.

basic_recursive_mutex wraps basic_mutex, but also storing the locking thread
id, and the recursion count. If the same thread tries to reacquire the lock,
it just increases the count, and it only releases the lock when the count hits
zero.

The next challenge is timed_mutex --- this cannot be written as an extension
of the current scheme, since that would expose a race condition. I might have
to revert to my lightweight_mutex scheme for that.

I also have a "checked" version of basic_mutex (not in CVS), that checks which
thread owns the mutex, and throws if the same thread tries to lock it twice,
or a thread that doesn't own it tries to unlock it. This requires additional
storage for the ID of the locking thread, and also has the overhead of the
additional checks. Does anyone think it's worth adding?

Anthony

-- 
Anthony Williams
Software Developer
Just Software Solutions Ltd
http://www.justsoftwaresolutions.co.uk

Threads-Devel list run by bdawes at acm.org, david.abrahams at rcn.com, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk