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.