ISO/IEC JTC1 SC22 WG14 N1414 - 2009-10-19
Lawrence Crowl, crowl@google.com, Lawrence@Crowl.org
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
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: C++ should incorporate the C definitions by reference at the appropriate time.
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. These functions should do no harm to a object that has been default-initialized.
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 explicit finalization functions or, preferably, accept the C functions. These functions should do no harm on objects that are later destroyed.
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: C++ should define a thread exit function so that it can specify its behavior with respect to destruction of thread-local storage.
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.
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. Note that the C approach may incur implementation inefficiencies on Mac OS X.
Recommendation: C and C++ should agree on a strategy for specifying mutex behavior.
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.
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.
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.
Discussion:
The approach taken by C++ is simply not available in C.
The primary issue is whether the move-only semantics of C++ thread
has sufficient value to justify an approach unavailable to C.
The recommendation below assumes such value.
Recommendation:
Make the C++ 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 thread::id
.
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.
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.
Recommendation: When C++ incorporates the C library by reference, it should define the thread-specific-storage destructors to execute before the destructors for thread-local objects.