C and C++ Thread Compatibility

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.

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

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

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

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

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.

Destruction

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.

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.

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.

  • Make the C type correspond to recursive_timed_mutex. This approach does not address any performance problems in C.
  • Split the C type into separate types corresponding to the C++ types.

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.

Observation: This idempotent initialization may introduce overhead.

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.

Observation: This idempotent initialization may introduce overhead.

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.

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.

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 Function Return

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.

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.

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.