ISO/IEC JTC1 SC22 WG14 N1423 - 2009-11-08
ISO/IEC JTC1 SC22 WG21 N2985 = 09-0175 - 2009-11-08
Lawrence Crowl, [email protected], [email protected]
Introduction
General Problems and Recommendations
Critical Compatiblity
Operations on Self
Quick Exit
Important Compatiblity
Thread-Local Storage
Call Once
Mutex
Condition
Desirable Compatiblity
Thread
Thread-Specific Storage
This paper revises WG14 N1414. It includes comments, observations, and recommenations from various sources, among them Hans Boehm, Peter Dimov, Howard Hinnant, Daniel Krügler, Tom Plum, Douglas Walls, Anthony Williams, and the Posix/C++ binding group. Their contribution should not necessarily be construed as an endorsement of anything within this paper. Furthermore, this document contains observations and recommendations, not a proposal.
The compatibility between the C and C++ threading facilities is important to many members of the respective communities. There are at least three levels of compatibility: critical, important, and desirable. Of these, the C and C++ committees should commit to achieving critical and important compatibility. This paper analyses the compatiblity between current draft standards, and recommends several actions to improve that compatibility.
The most useful kind of compatibility is when an application header using thread facilities can be included by and used from both languages. Furthermore, it is desirable for C++ programs to be able to use C++ syntax with objects from that header. The recommendations within this paper support that goal.
There are several problems that span all facilities. Later discussion may provide more specific discussion.
C++ does not recognize the C definitions.
Recommendation: Functions in C but not in C++ should be incorporated by reference. If the C standard happens before the C++ standard, this reference will be within the standard. Otherwise, this reference will be within a Technical Report subsequent to the standard.
There is at present no guarantee that C and C++ concurrency objects have the same representation.
Recommendation: Where possible, make that guarantee, preferably by using the same type name.
The initialization of objects is not compatible. In particular, the C++ default initialization syntax fails to initialize C objects and C++ does not recognize the C initialization functions. Furthermore, the C standard fails to define the result of access to a zero-initialized global concurrency object. Well-defined behavior here is important because of the indeterminate nature of intialization function order.
Recommendation: C should specify the meaning of a zero-initialized synchronization object. Preferably, the zero-initialization of such objects should be the ready-to-use state. Failing that, C should provide for explicit static initialization of all concurrency objects. C++ should add explicit initialization functions, or, preferably, accept the C functions. These functions should do no harm to a object that has been default-initialized. That is, the initialization functions are idempotent with constructors.
C++ prevents copying from (including parameter passing) and assignment to concurrency objects. C fails to define the semantics of such actions.
Recommendation: C should specify copying and assignment of such objects as undefined behavior.
The finalization of objects is not compatible. In particular, the C will not execute C++ destructors and may therefore need an explicit finalization call.
Recommendation: C++ should either add explicit finalization functions or, preferably, accept the C functions. These functions should do no harm on objects that are later destroyed. That is, the finalization functions are idempotent with destructors.
C reports errors through a return value. C++ reports errors through exceptions.
Recommendation: No action; these approaches are appropriate to each language.
C defines many functions with int
parameter and return types,
even though the values correspond to enumerators.
This weakening of types reduces diagnostic capability.
Recommendation: C should define enumeration types for mutex behavior and return status and then use those in the definition of the functions.
C++ provides a mechanism to obtain the native operating-system handle for various concurrency objects, which makes platform-specific tweaks possible. C provides no such mechanism.
Recommendation: C should consider adding this facility.
The critical level of compatibility is that a C thread is a C++ thread, and that operations on one's own thread apply to that thread regardless of the language used to create the thread. While it is difficult to state this requirement normatively, it is a reasonable expectation on the part of users.
The operations that a thread may perform on itself are as follows.
C | C++ |
---|---|
void thrd_yield( void); |
std::this_thread::yield(); |
void thrd_sleep( const xtime *xt); |
template< class Clock, class Duration> |
no facility | template< class Rep, class Period> |
void thrd_exit( int res); |
no facility |
C is missing a duration sleep function.
It may appear that one could easily
synthesize the behavior by adding an offset to
the result of xtime_get
,
but subtle effects of clock resetting
make that synthesis not accurate.
Recommendation: No action at this time.
C++ is missing a thread_exit
function.
Recommendation:
There seems to be general agreement among C++ experts
that a thread exit that does not unwind the stack will be a resource leak.
This leak is likely to also exist in C
unless the programmers are very careful.
The issue does not apply to process exit
because the operating system
generally cleans up resources on process exit anyway.
Further, "uncatchable" exceptions also seem to not help.
Therefore, any
So, either C should not define thread_exit
or C++ should define a thread exit function
that throws some standard exception.
Programmers would need to ensure that the exeception is handled reasonably.
The operations that applications may use to exit the program without synchronizing threads.
C | C++ |
---|---|
int at_quick_exit( void (*f)(void)); |
extern "C" int at_quick_exit( void (*f)(void)); |
void quick_exit( int status); |
void quick_exit [[noreturn]]( int status); |
The C standard is missing pending C++ clarifications, but otherwise the standards are fully compatible.
Recommendation: Track clarifications between the two languages.
The important level of compatiblity is that C and C++ code be able to communicate through the same objects. (We ignore atomic objects in this paper, and concentrate on other objects.)
The facilities for thread-duration variables are as follows.
C | C++ |
---|---|
_Thread_local |
thread_local |
The C storage class specifier for thread-local storage
is _Thread_local
.
In contrast, the C++ specifier is thread_local
.
These are not compatible.
Recommendation:
Add an adaptation header to C,
much like much like <stdbool>
,
that #define
s thread_local
as _Thread_local
.
In C++, this header would be empty.
Some C headers may not be able to include this adaptation header
for legacy reasons,
so C++ should add _Thread_local
as an alternate keyword for thread_local
.
In C++, thread-local variables can only be named by the current thread, but they can be accessed indirectly from any thread. In contrast, access to C thread-local variables from another thread is implementation defined. This behavior is one-way compatible — programs obeying C rules will execute correctly under C++.
Recommendation: No change.
In C++, inline function definitions may contain static and thread storage duration variable definitions. In C, they may not. This behavior is one-way compatible — programs obeying C rules will execute correctly under C++.
Recommendation: No change.
C++ supports thread-local variables with destructors. C has no equivalent concept. At the coarse level, there is no incompatiblity as C types have trivial destructors.
Observation:
C++ may introduce user-level facilities
for controling the timing of destruction,
which would introduce additional overhead
on thread-local variables with non-trivial destructors
and may introduce additional overhead
on thread-local variables with trivial destructors, i.e. C types.
Such additional implementation may be incompatible
with existing __thread
implementations.
Recommendation:
If C++ introduces a mechanism for thread_local
that is incompatible with existing __thread
variables,
C++ should consider supporting two such facilities.
In both C and C++, there is no guarantee that a signal will be handled by any particular thread, and therefore signal handlers must not rely on the identity of thread-local storage. This behavior is fully compatible.
Recommendation: No change.
The facilities for executing a function once are as follows.
C | C++ |
---|---|
typedef object-type once_flag; |
struct once_flag; |
once_flag var = ONCE_FLAG_INIT; |
once_flag var; |
void call_once( once_flag *flag, void (*func)(void)); |
template< class Callable, class ...Args> |
The types are compatible,
provided the C standard typedefs once_flag
to struct once_flag
.
Recommendation: No change.
The initialization of once_flag
objects is not compatible.
In particular, the C++ syntax fails to initialize a C object
and C++ does not recognize the C initialization syntax.
(The C standard fails to define
the result of access to an uninitialized once_flag
object.)
Recommendation:
C should specify the meaning of an unitialized once_flag
.
Preferably, it should define zero-initialization as not-yet-executed.
C++ should add constexpr
constructor
accepting a ONCE_FLAG_INIT
value.
The facilities for mutual exclusion are as follows.
C | C++ |
---|---|
typedef object-type mtx_t; |
class mutex;
class recursive_mutex;
class timed_mutex;
class recursive_timed_mutex; |
int mtx_init( mtx_t *mtx, int type); given a type of
mtx_plain , mtx_timed , mtx_try ,
mtx_plain|mtx_recursive , mtx_timed|mtx_recursive ,
or mtx_try|mtx_recursive
|
default constructor |
void mtx_destroy( mtx_t *mtx); |
destructor |
int mtx_unlock( mtx_t *mtx); |
void mutex::unlock(); |
int mtx_lock( mtx_t *mtx); |
void mutex::lock(); |
int mtx_trylock( mtx_t *mtx); |
void mutex::try_lock(); |
int mtx_timedlock( mtx_t *mtx,
const xtime *xt); |
template< class Clock, class Duration> |
no facility |
template< class Rep, class Period> |
C defines the behavior of a mutex object by initialization. C++ defines the behavior by static type. This is a serious incompatibility.
Observation: The C approach may incur implementation inefficiencies on some platforms for the plain mutex use. The implementation on Mac OS X is known to be less efficient.
Observation:
The performance of many concurrent applications
is strongly affected by the time to obtain and release an uncontended lock.
The C++ lock_guard
was designed expressly to minimize that time
by avoiding any conditional execution.
A mutex type with dynamic behavior
will necessarily reintroduce conditional execution.
Observation: In the multiple-type approach, if you have a recursive mutex, you cannot pass it to a function requiring a plain mutex. In C++, this problem is generally solved by making the function a template function. That solution is not available to C.
Recommendation: C and C++ should agree on a strategy for specifying mutex behavior. Two main approaches have been suggested.
recursive_timed_mutex
.
This approach does not address any performance problems in C.
The mutex type names are incompatible, but that problem is secondary to the above problem.
Recommendation: C and C++ should agree on the type name(s). Failing that, C++ should define the C names as typedefs to the C++ classes.
C does not specify the semantics of zero-initialized mutexes. C++ initializes mutexes by construction. C initializes mutexes with a separately called function. These approaches are incompatible.
Recommendation: C should define zero-initialization as unlocked. C++ should define an initialization function, even if that function is redundant with respect to construction. At the very least, C++ should provide specific semantics when incorporating the C library by reference.
Observation: This idempotent initialization may introduce overhead.
C++ destroys mutexes by destruction. C destroys mutexes with a separately called function. These approaches are incompatible.
Recommendation: C++ should define a destroy function, even if that function is redundant with respect to destruction. At the very least, C++ should provide specific semantics when incorporating the C library by reference.
Observation: This idempotent initialization may introduce overhead.
C defines mutexes with a try-lock operation as a separate kind of mutex, whereas C++ incorporates that operation into all mutexes. This separation seems unnecessarily restrictive.
Recommendation:
C should remove mtx_timed
and permit mtx_trylock
on all mutexes.
C is missing a duration lock function. See the sleep function discussion.
Recommendation: No action at this time.
The facilities for conditional waiting are as follows.
C | C++ |
---|---|
typedef object-type cnd_t; |
class condition_variable; |
int cnd_init( cnd_t *cond); |
default constructor |
void cnd_destroy( cnd_t *cond); |
destructor |
int cnd_signal( cnd_t *cond); |
void condition_variable::notify_one(); |
int cnd_broadcast( cnd_t *cond); |
void condition_variable::notify_all(); |
int cnd_wait( cnd_t *cond, mtx_t *mtx); |
void condition_variable::wait( unique_lock< mutex> lock); |
int cnd_timedwait( cnd_t *cond, mtx_t *mtx,
const xtime *xt); |
template< class Clock, class Duration> |
no facility |
template< class Rep, class Period> |
The C and C++ type names are incompatible.
Recommendation: C and C++ should agree on the type name. Failing that, C++ should define the C name as a typedef to the C++ class.
C does not specify the semantics of zero-initialized condition variables. C++ initializes condition variables by construction. C initializes condition variables with a separately called function. These approaches are incompatible.
Recommendation: C should define zero-initialization as no notifications. C++ should define an initialization function, even if that function is redundant with respect to construction. At the very least, C++ should provide specific semantics when incorporating the C library by reference.
C++ destroys condition variables by destruction. C destroys condition variables with a separately called function. These approaches are incompatible.
Recommendation: C++ should define a destroy function, even if that function is redundant with respect to destruction. At the very least, C++ should provide specific semantics when incorporating the C library by reference.
C is missing a duration wait function. See the sleep function discussion.
Recommendation: No action at this time.
The desirable level of compatibility is that C and C++ can operate on each other's threads.
The facilities for creating and managing threads are as follows.
C | C++ |
---|---|
typedef object-type thrd_t; |
class thread; |
typedef int (*thrd_start_t)( void*); |
thread::thread( template< class F> explicit thread( F f);
|
no facility | bool thread::joinable(); |
int thrd_join( thrd_t thr, int *res); |
void thread::join(); |
int thrd_detach( thrd_t thr); |
void thread::detach(); |
not applicable | thread::id thread::get_id() |
thrd_t thrd_current( void); |
thread::id this_thread::get_id(); |
int thrd_equal( thrd_t thr0, thrd_t thr1); |
bool operator==( thread::id x, thread::id y); |
no facility | other thread::id relational operators |
C provides operations on threads through a handle type. In contrast, C++ provides operations directly on a move-only object type. C++ does provide a handle type, but operations on it are limited to identity checks. These approaches are not directly compatible. Because these types are not compatible, there is no mechanism to operate on threads created in C++ from C or vice versa.
Observation:
The move-only approach taken by C++ is simply not available directly in C.
However, C could restrict the use of thrd_t
to correspond to a move-only type.
The primary issue is whether the move-only approach
has sufficient value to justify the approach in both languages,
the approach in neither language,
or an incompatibility between languages.
Recommendation:
Make the C++ std::thread::id
type
be a typedef to the same type as the C thrd_t
typedef.
This recommendation does open an avenue
to joining/detaching a thread
without owning the thread
object
via using thrd_join
or thrd_detach
on the std::thread::id
.
Observation: The recommendation above removes the strong guarantee of the C++ thread type.
Recommendation:
C should add a distinct thread_id_t type
which is compatible with std::thread::id
,
along with a thrd_get_id()
function.
thrd_current
should return this thread_id_t
type,
not thrd_t
.
The C++ types thread
and thread::id
have null values.
The C types do not.
This value is important for data structures referencing threads.
Recommendation:
C should define the syntax for defining a null thrd_t
.
Preferably, one would obtain the null value from zero initialization.
C should define thrd_t
comparison
to work with these null values.
C++ provides a query on thread
to see if it is still legally joinable.
C has no such facility.
This function is related to the null-value issue.
Recommendation: C should define such a function.
In C, thread functions may return an int
,
which is passed through to the return value of thrd_join
.
In C++, any return value is lost.
Recommendation: There are two possible recommendations. First, do nothing. Second, note that the return value is intended to support thread exit; so if thread exit is not supported, simply remove the return value from the thread return.
The facilities for thread-specific storage are as follows.
C | C++ |
---|---|
typedef object-type tss_t; |
no facility |
#define TSS_DTOR_ITERATIONS
integer-constant-expression |
no facility |
typedef void (*tss_dtor_t)( void*) |
no facility |
int tss_create( tss_t *key, tss_dtor_t dtor); |
no facility |
void tss_delete( tss_t key); |
no facility |
void *tss_get( tss_t key); |
no facility |
int tss_set( tss_t key, void *val); |
no facility |
C++ does not provide thread-specific storage.
Observation
C++ requires that
thread_local
destructors
run before atexit
handlers,
whereas POSIX TSD destructors run as the last code executed by the thread,
after atexit
handlers
if the current thread called exit
.
It looks like the C functions
are intended to map cleanly onto their POSIX equivalent,
which would imply that
they would run after C++ thread_local
destructors,
not before.
Recommendation: When C++ incorporates the C library by reference, it should define the thread-specific-storage destructors execution relative to destructors for thread-local objects. On possibility is to not define this ordering.