C and C++ Thread Compatiblity

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

Introduction

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.

General Problems and Recommendations

There are several problems that span all facilities. Later discussion may provide more specific discussion.

Incorporation

C++ does not recognize the C definitions.

Recommendation: C++ should incorporate the C definitions by reference at the appropriate time.

Representation

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.

Initialization

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.

Copying and Assignment

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.

Finalization

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.

Error Reporting

C reports errors through a return value. C++ reports errors through exceptions.

Recommendation: No action; these approaches are appropriate to each language.

Enumerations

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.

Native Handle

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.

Critical Compatiblity

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.

Operations on Self

The operations that a thread may perform on itself are as follows.

CC++
void thrd_yield( void); std::this_thread::yield();
void thrd_sleep( const xtime *xt); template< class Clock, class Duration>
void std::this_thread::sleep_until( const chrono::time_point< Clock, Duration>& abs_time);
no facility template< class Rep, class Period>
void std::this_thread::sleep_for( const chrono::duration< Rep, Period>& rel_time);
void thrd_exit( int res); no facility

Duration

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.

Thread Exit

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.

Quick Exit

The operations that applications may use to exit the program without synchronizing threads.

CC++
int at_quick_exit( void (*f)(void)); extern "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.

Important Compatiblity

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.)

Thread-Local Storage

The facilities for thread-duration variables are as follows.

CC++
_Thread_local thread_local

Keyword

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 #defines 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.

Remote Access

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.

Local Variables

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.

Signals

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.

Call Once

The facilities for executing a function once are as follows.

CC++
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>
void call_once( once_flag& flag, Callable func, Args&&... args);

Type Name

The types are compatible, provided the C standard typedefs once_flag to struct once_flag.

Recommendation: No change.

Initialization

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.

Mutex

The facilities for mutual exclusion are as follows.

CC++
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>
bool mutex::try_lock_until( const chrono::time_point< Clock, Duration>& abs_time);
no facility template< class Rep, class Period>
bool mutex::try_lock_for( const chrono::duration< Rep, Period>& rel_time);

Behavior

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.

Type Names

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.

Initialization

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.

Finalization

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.

Try Lock

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.

Duration Lock

C is missing a duration lock function. See the sleep function discussion.

Recommendation: No action at this time.

Condition

The facilities for conditional waiting are as follows.

CC++
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>
bool condition_variable::wait_until( unique_lock< mutex> lock, const chrono::time_point< Clock, Duration>& abs_time);
no facility template< class Rep, class Period>
bool condition_variable::wait_for( unique_lock< mutex> lock, const chrono::duration< Rep, Period>& rel_time);

Type Names

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.

Initialization

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.

Finalization

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.

Duration Wait

C is missing a duration wait function. See the sleep function discussion.

Recommendation: No action at this time.

Desirable Compatiblity

The desirable level of compatibility is that C and C++ can operate on each other's threads.

Thread

The facilities for creating and managing threads are as follows.

CC++
typedef object-type thrd_t; class thread;
typedef int (*thrd_start_t)( void*);
int thrd_create( thrd_t *thr, thrd_start_t func, void *arg);
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

Type Approach

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.

Null Values

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.

Joinable Query

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.

Thread-Specific Storage

The facilities for thread-specific storage are as follows.

CC++
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.