| Document Number: | WG14 N1437 |
| Date: | 2010-02-05 |
| Author: | Anthony
Williams Just Software Solutions Ltd |
The current C1x draft (WG14 N1425) has a single mutex type
(mtx_t), the properties of which are specified when
each instance is initialized. On the other hand, the C++0x draft
(WG21 N3000) has 4 distinct mutex types
(std::mutex, std::timed_mutex, std::recursive_mutex
and std::recursive_timed_mutex), each with predefined
properties.
This discrepancy was highlighted by Lawrence Crowl in his paper on C and C++ compatibility in the thread library (WG14 N1423, WG21 N2985), and has since led to much discussion on the C and C++ compatibility reflector, and a series of informal teleconferences. This paper has come out of these discussions.
There are two primary issues at stake here:
The usual way for both C and C++ code to operate on a single type whilst retaining C++ idioms for C++ code is to have the C++ member functions map to C global functions that take the object address as the first parameter. However, this presupposes that there is a single common type that shares the same set of operations.
For the mutexes as currently provided in the two draft standards this is not the case. Though it is possible to implement the C++ mutex types in terms of the C mutex type, or indeed to implement the C mutex type in terms of the C++ types, the disparate types, and the disparate set of operations that can be applied means that there is no one-to-one mapping that can be made.
If the C mtx_t type was to be mapped to the
C++ std::mutex type, then C++ code would not be able
to take advantage of the timed-wait operations on that mutex, even
if the mutex was initialized in C to allow such operations by
passing the mtx_timed flag
to mtx_init. Conversely, the
C++ std::mutex type always
supports try_lock operations, which would only be
supported by the C mtx_t type if
the mtx_try flag was passed
to mtx_init.
Similar problems occur if you map the C mtx_t type
to the C++ std::recursive_timed_mutex type: though
the C++ code can now access every operation theoretically
supported by the C type, this may be a lie in practice if
the mtx_t was not initialized with the full set of
flags.
Such a lie is more of a problem in C++ code than in C code,
since the distinct mutex types guarantee to support the specified
operations, whereas a C programmer must be aware that
the mtx_t type doesn't necessarily support all the
operations, and so he must ensure that the necessary flags have
been supplied.
The compatibility of mutex types doesn't just affect mutexes;
it affects condition variables too. On the C++
side, condition_variable::wait() requires that
a unique_lock<mutex> be passed, whereas on
the C side a mtx_t* must be passed
to cnd_wait. Combined with the requirement that all
calls to wait() pass the same mutex, this is
clearly a problem if a single condition variable is to be shared
between C and C++ code. If the single mtx_t is
replaced with four distinct mutex types for compatibility with
C++ then this problem goes away —
the cnd_wait function can just take
a mutex* for symmetry with the C++ code.
Though this paper arose out of the desire for compatibility between C and C++, I think that the performance of C code that uses mutexes is potentially a more important issue. This performance issue is one of the arguments that led to the provision of distinct mutex types in the C++ standard, and the issues are just as important for C as for C++.
The key issue is this: on some platforms adding support for locks with timeouts and/or support for recursive locking is expensive both in terms of the size of the mutex object and in terms of the performance of operations upon that object. There is thus considerable advantage to be gained from having distinct types: you only pay the cost of the additional operations if you need them.
The C draft goes someway to acknowledging this fact by allowing for mutexes with distinct properties which are specified when each instance is initialized. However, this limits the scope for such performance gains.
In the first instance, it means that either
the mtx_t type must be large enough to encompass all
the data required for the largest mutex variant, or there must be
some amount of dynamic storage allocated in such cases (thus
adding to the cost of using the mutex).
Secondly, each operation for which the code varies between the
mutex variants must now perform some kind of test and branch to
check which piece of code to use: does this mutex support timed
operations (which therefore requires the use of the slow code with
support for such operations), or does it not (so we can use the
fast code without such support)? Does this mutex support recursive
operations (so we must call the slow code to keep track of the
requisite tracking information), or does it not (so we can omit
the code to handle this tracking information)? Whether this is
coded as a test-and-branch or a call through a function pointer,
there is still an overhead. The same applies for each possible
feature set — on several platforms the support of locks with
timeouts requires a more complex implementation, so even the plain
locking code must change. This is apparent in the Boost
implementation of boost::timed_mutex (see the source
code for POSIX platforms
at https://svn.boost.org/trac/boost/browser/branches/release/boost/thread/pthread/mutex.hpp.)
As a comparison, I have implemented both the mtx_t
from the current C1x working draft and my proposed C++-compatible
mutex types for linux. On a simple lock-unlock of an uncontended
mutex the performance benefit of mutex_t
over mtx_t is clearly noticeable: on my system,
100000000 lock/unlock pairs takes 4.18s for mutex_t,
4.97s for mtx_t and 5.56s
for pthread_mutex_t (implying 41.8ns vs 49.7ns vs
55.6ns per lock/unlock pair). The separate mutex_t thus
provides a 30% improvement over pthread_mutex_t and
an 18% improvement over mtx_t (which differs only by a
test and branch), which is not to be sniffed at.
My tests also show the timings for recursive mutex locking,
with two locks followed by two unlocks for each cycle. In this case,
my recursive_mutex_t implementation yielded 5.31s for
the 100000000 cycles, whereas pthread_mutex_t took 7.14s
and my mtx_t took 5.94s. Again we see the cost of the
extra test-and-branch to optimize the non-recursive case
of mtx_t, in that it is slower than a
plain recursive_mutex_t, but it is still faster than the
use of pthread_mutex_t.
Note that my tests have focused on the uncontended case: in the contended case then the cost of a blocking call will tend to mask such small differences in performance. All tests were run on Kubuntu 9.10 x64 with an Intel Core 2 Duo 1.83Ghz CPU.
The following edits are based on the C1x draft in WG14 paper N1425.
Remove the type mtx_t from 7.24p3, and replace it with
the
types mutex_t, recursive_mutex_t, timed_mutex_t
and recursive_timed_mutex_t:
cnd_twhich is an object type that holds an identifier for a condition variable;
thrd_twhich is an object type that holds an identifier for a thread;
tss_twhich is an object type that holds an identifier for a thread-specific storage pointer;
which is an object type that holds an identifier for a plain mutex;mtx_tmutex
recursive_mutexwhich is an object type that holds an identifier for a recursive mutex;
timed_mutexwhich is an object type that holds an identifier for a mutex that supports locks with timeouts;
recursive_timed_mutexwhich is an object type that holds an identifier for a recursive mutex that supports locks with timeouts;
tss_dtor_twhich is the function pointer type
void (*)(void*), used for a destructor for a
thread-specific storage pointer;
thrd_start_twhich is the function pointer type
int (*)(void*) that is passed to thrd_create
to create a new thread;
once_flagwhich is an object type that holds a flag for use by
call_once; and
xtimewhich is a structure type that holds a time specified in seconds and nanoseconds. The structure shall contain at least the following members, in any order.
time_t sec; long nsec;
Remove the mutex type enumeration constants from 7.24p4:
mtx_plainwhich is passed to
mtx_init to create a mutex object that supports neither timeout nor
test and return;
mtx_recursivewhich is passed to
mtx_init to create a mutex object that supports recursive locking;
mtx_timedwhich is passed to
mtx_init to create a mutex object that supports timeout;
mtx_trywhich is passed to
mtx_init to create a mutex object that
supports test and return;
thrd_timeoutwhich is returned by a timed wait function to indicate that the time specified in the call was reached without acquiring the requested resource;
thrd_successwhich is returned by a function to indicate that the requested operation succeeded;
thrd_busywhich is returned by a function to indicate that the requested operation failed because a resource requested by a test and return function is already in use;
thrd_errorwhich is returned by a function to indicate that the requested operation failed; and
thrd_nomemwhich is returned by a function to indicate that the requested operation failed because it was unable to allocate memory.
Change the references to mtx_t in 7.24.3.5 and
7.24.3.6 to mutex_t:
cnd_timedwait functionint cnd_timedwait(cnd_t *cond,mtx_tmutex *mtx,const xtime *xt);
cnd_wait functionint cnd_wait(cnd_t *cond,mtx_tmutex *mtx);
Replace the entirety of 7.24.4 with the following:
mutex_destroy functionvoid mutex_destroy(mutex_t *mtx);
mutex_destroy function releases any resources used by the mutex pointed to by
mtx. No threads can be blocked waiting for the mutex pointed to by mtx.mutex_destroy function returns no
value.mutex_init functionint mutex_init(mutex_t *mtx);
mutex_init function creates a mutex object. If
the mutex_init function succeeds, the mutex pointed
to by mtx is initialized to a valid mutex that is
distinct from all other mutexes in the program.mutex_init function
returns thrd_success on success,
or thrd_error if the request could not be
honored.mutex_lock functionint mutex_lock(mutex_t *mtx);
mutex_lock function blocks until it locks the mutex pointed to by mtx. The mutex shall not be locked by the calling thread. Prior calls to mutex_unlock
on the same mutex shall synchronize with this operation.mutex_lock function
returns thrd_success on success,
or thrd_busy if the resource requested is already in use,
or thrd_error if the request could not be
honored.mutex_try_lock functionint mutex_try_lock(mutex_t *mtx);
mutex_try_lock function endeavors to lock the mutex pointed to by mtx. If the
mutex is already locked, the function returns without blocking. Prior calls to
mutex_unlock on the same mutex shall synchronize with this operation.mutex_try_lock function returns thrd_success on success, or thrd_busy if
the resource requested is already in use, or thrd_error if the request could not be
honored.mutex_unlock functionint mutex_unlock(mutex_t *mtx);
mutex_unlock function unlocks the mutex pointed to by mtx. The mutex pointed to
by mtx shall be locked by the calling thread.mutex_unlock function returns thrd_success on success or thrd_error if
the request could not be honored.recursive_mutex_destroy functionvoid recursive_mutex_destroy(recursive_mutex_t *mtx);
recursive_mutex_destroy function releases any resources used by the mutex pointed to by
mtx. No threads can be blocked waiting for the mutex pointed to by mtx.recursive_mutex_destroy function returns no
value.recursive_mutex_init functionint recursive_mutex_init(recursive_mutex_t *mtx);
recursive_mutex_init function creates a mutex object. If
the recursive_mutex_init function succeeds, the mutex pointed
to by mtx is initialized to a valid mutex that is
distinct from all other mutexes in the program.recursive_mutex_init function
returns thrd_success on success,
or thrd_error if the request could not be
honored.recursive_mutex_lock functionint recursive_mutex_lock(recursive_mutex_t *mtx);
recursive_mutex_lock function blocks until it
locks the mutex pointed to by mtx. If the mutex is
already locked by the calling thread then the call shall return
without blocking. Prior calls to recursive_mutex_unlock
on the same mutex shall synchronize with this operation. recursive_mutex_lock function
returns thrd_success on success,
or thrd_busy if the resource requested is already in use,
or thrd_error if the request could not be
honored.recursive_mutex_try_lock functionint recursive_mutex_try_lock(recursive_mutex_t *mtx);
recursive_mutex_try_lock function endeavors to
lock the mutex pointed to by mtx. If the mutex is already
locked, the function returns without blocking. Prior calls to
recursive_mutex_unlock on the same mutex shall
synchronize with this operation.recursive_mutex_try_lock function
returns thrd_success on success,
or thrd_busy if the resource requested is already in use,
or thrd_error if the request could not be
honored.recursive_mutex_unlock functionint recursive_mutex_unlock(recursive_mutex_t *mtx);
recursive_mutex_unlock function unlocks the
mutex pointed to by mtx. The mutex pointed to
by mtx shall be locked by the calling thread.recursive_mutex_unlock function returns thrd_success on success or thrd_error if
the request could not be honored.timed_mutex_destroy functionvoid timed_mutex_destroy(timed_mutex_t *mtx);
timed_mutex_destroy function releases any resources used by the mutex pointed to by
mtx. No threads can be blocked waiting for the mutex pointed to by mtx.timed_mutex_destroy function returns no
value.timed_mutex_init functionint timed_mutex_init(timed_mutex_t *mtx);
timed_mutex_init function creates a mutex object. If
the timed_mutex_init function succeeds, the mutex pointed
to by mtx is initialized to a valid mutex that is
distinct from all other mutexes in the program.timed_mutex_init function
returns thrd_success on success,
or thrd_error if the request could not be
honored.timed_mutex_lock functionint timed_mutex_lock(timed_mutex_t *mtx);
timed_mutex_lock function blocks until it locks the mutex pointed to by mtx. The mutex shall not be locked by the calling thread. Prior calls to timed_mutex_unlock
on the same mutex shall synchronize with this operation.timed_mutex_lock function
returns thrd_success on success,
or thrd_busy if the resource requested is already in use,
or thrd_error if the request could not be
honored.timed_mutex_try_lock functionint timed_mutex_try_lock(timed_mutex_t *mtx);
timed_mutex_try_lock function endeavors to lock the mutex pointed to by mtx. If the
mutex is already locked, the function returns without blocking. Prior calls to
timed_mutex_unlock on the same mutex shall synchronize with this operation.timed_mutex_try_lock function returns thrd_success on success, or thrd_busy if
the resource requested is already in use, or thrd_error if the request could not be
honored.timed_mutex_try_lock_until functionint timed_mutex_try_lock_until(timed_mutex_t *mtx, const xtime *xt);
timed_mutex_try_lock_until function endeavors to
block until it locks the mutex pointed to by mtx or
until the point in time specified by the xtime
object xt has passed. If the mutex is already locked,
the function returns without blocking. Prior calls to
timed_mutex_unlock on the same mutex shall
synchronize with this operation.timed_mutex_try_lock_until function
returns thrd_success on success,
or thrd_busy if the resource requested is already in
use, or thrd_timeout if the point in time specified
was reached without acquiring the requested resource,
or thrd_error if the request could not be
honored.timed_mutex_unlock functionint timed_mutex_unlock(timed_mutex_t *mtx);
timed_mutex_unlock function unlocks the mutex pointed to by mtx. The mutex pointed to
by mtx shall be locked by the calling thread.timed_mutex_unlock function returns thrd_success on success or thrd_error if
the request could not be honored.recursive_timed_mutex_destroy functionvoid recursive_timed_mutex_destroy(recursive_timed_mutex_t *mtx);
recursive_timed_mutex_destroy function releases any resources used by the mutex pointed to by
mtx. No threads can be blocked waiting for the mutex pointed to by mtx.recursive_timed_mutex_destroy function returns no
value.recursive_timed_mutex_init functionint recursive_timed_mutex_init(recursive_timed_mutex_t *mtx);
recursive_timed_mutex_init function creates a mutex object. If
the recursive_timed_mutex_init function succeeds, the mutex pointed
to by mtx is initialized to a valid mutex that is
distinct from all other mutexes in the program.recursive_timed_mutex_init function
returns thrd_success on success,
or thrd_error if the request could not be
honored.recursive_timed_mutex_lock functionint recursive_timed_mutex_lock(recursive_timed_mutex_t *mtx);
recursive_timed_mutex_lock function blocks until it
locks the mutex pointed to by mtx. If the mutex is
already locked by the calling thread then the call shall return
without blocking. Prior calls to recursive_timed_mutex_unlock
on the same mutex shall synchronize with this operation. recursive_timed_mutex_lock function
returns thrd_success on success,
or thrd_busy if the resource requested is already in use,
or thrd_error if the request could not be
honored.recursive_timed_mutex_try_lock functionint recursive_timed_mutex_try_lock(recursive_timed_mutex_t *mtx);
recursive_timed_mutex_try_lock function endeavors to
lock the mutex pointed to by mtx. If the mutex is already
locked, the function returns without blocking. Prior calls to
recursive_timed_mutex_unlock on the same mutex shall
synchronize with this operation.recursive_timed_mutex_try_lock function
returns thrd_success on success,
or thrd_busy if the resource requested is already in use,
or thrd_error if the request could not be
honored.recursive_timed_mutex_try_lock_until functionint recursive_timed_mutex_try_lock_until(recursive_timed_mutex_t *mtx, const xtime *xt);
recursive_timed_mutex_try_lock_until function
endeavors to block until it locks the mutex pointed to
by mtx or until the point in time specified by
the xtime object xt has passed. If the
mutex is already locked, the function returns without
blocking. Prior calls to
recursive_timed_mutex_unlock on the same mutex shall
synchronize with this operation.recursive_timed_mutex_try_lock_until function
returns thrd_success on success,
or thrd_busy if the resource requested is already in
use, or thrd_timeout if the point in time specified
was reached without acquiring the requested resource,
or thrd_error if the request could not be
honored.recursive_timed_mutex_unlock functionint recursive_timed_mutex_unlock(recursive_timed_mutex_t *mtx);
recursive_timed_mutex_unlock function unlocks the
mutex pointed to by mtx. The mutex pointed to
by mtx shall be locked by the calling thread.recursive_timed_mutex_unlock function returns thrd_success on success or thrd_error if
the request could not be honored.A sample implementation of much of the proposed API, along
with much of the mtx_t API from N1425 is available at
http://www.justsoftwaresolutions.co.uk/files/c_mutexes.zip,
licensed under the GPL v3.