Introduction
Even though nascent “threads” appeared as early as 1966, they weren’t supported by any major programming language of that era. POSIX threads, aka pthreads (“pea-threads”), didn't appear until 1995.
Consequently when C was created in 1972, it didn’t support threads at all until pthreads came along. Pthreads is widely available on any Unix system and even Microsoft Windows ports exist.
Among other things, C11 added standard library support for threads. Its API is necessarily superficially different from and a subset of pthreads.
While that’s not terrible, it’s unfortunate; more unfortunate is that standard threads are optional for implementations. Specifically, if the compiler predefines the __STDC_NO_THREADS__
macro, then standard threads are not supported. Hence, if you’re writing cross-platform software, you can’t rely on standard threads being available.
So what should you do? You certainly don’t want to use #ifdef __STDC_NO_THREADS__
all over and write the same lines of threading code twice: once using the standard API #else
using the pthreads API. You have two choices:
Forget about standard threads and just use pthreads since it’s more widely supported. This is a perfectly reasonable choice.
Implement a standard threads API wrapper around pthreads and use that if standard threads aren’t available. While such a wrapper is fairly trivial, it’s just more code for no real benefit.
Additionally, as mentioned, standard threads’ API is a subset of pthreads’ API. If you need to use any part of the pthreads API that’s not in the standard subset, you’re forced to use pthreads.
Despite these unfortunate realities, let’s implement such a wrapper.
Macros & Types
Defining macros and types that map from standard threads’s types to pthreads’ types is straightforward:
#define ONCE_FLAG_INIT PTHREAD_ONCE_INIT
enum {
mtx_plain = 0,
mtx_timed = 1 << 0,
mtx_recursive = 1 << 1
};
enum {
thrd_success,
thrd_busy = EBUSY,
thrd_nomem = ENOMEM,
thrd_timedout = ETIMEDOUT,
thrd_error
};
typedef pthread_cond_t cnd_t;
typedef pthread_mutex_t mtx_t;
typedef pthread_once_t once_flag;
typedef int (*thrd_start_t)( void* );
typedef pthread_t thrd_t;
typedef void (*tss_dtor_t)( void* );
typedef pthread_key_t tss_t;
Trivial Wrapper Functions
Most of the wrapper functions are one-liners that can be done inline, such as for cnd_signal
:
inline int cnd_signal( cnd_t *c ) {
return pthread_cond_signal( c ) == 0 ? thrd_success : thrd_error;
}
Such implementations can also be done for cnd_broadcast
, cnd_wait
, mtx_lock
, mtx_unlock
, thrd_current
, thrd_detach
, thrd_equal
, thrd_exit
, thrd_yield
, tss_create
, tss_get
, and tss_set
.
Other wrappers have to map error codes, such as for cnd_init
:
int cnd_init( cnd_t *c ) {
switch ( pthread_cond_init( c, /*attr=*/nullptr ) ) {
case 0 : return thrd_success;
case ENOMEM: return thrd_nomem;
default : return thrd_error;
}
}
Incidentally, the above code also gives an example of how the standard API is a subset of pthreads. Specifically, in pthreads, conditions, mutexes, and threads can all be created with optional attributes.
Such implementations can also be done for cnd_timedwait
, mtx_timedlock
, and mtx_trylock
.
There are a few functions in the standard threads API that have no return value even though their counterparts in the pthreads API return an error code, such as call_once
vs. pthread_once
. Granted, such pthreads functions fail only if you make a programming mistake, but ignoring errors is generally a bad idea. For such functions, we can do something like this:
void call_once( once_flag *flag, void (*once_fn)() ) {
ASSERT_IS_0( pthread_once( flag, once_fn ) );
}
where ASSERT_IS_0
is:
#ifndef NDEBUG
static void assert_is_0_err( char const *file, int line,
int expr, char const *expr_str ) {
fprintf( stderr,
"%s:%d: '%s' non-zero value %d: %s\n",
file, line, expr_str, expr, strerror( expr )
);
abort();
}
#define ASSERT_IS_0(EXPR) \
do { \
if ( unlikely( expr != 0 ) ) \
assert_is_0_err( __FILE__, __LINE__, (EXPR), #EXPR )
} while (0)
#else
#define ASSERT_IS_0(EXPR) (EXPR)
#endif /* NDEBUG */
(See here regarding unlikely
.)
We can’t simply use assert
because we always want the expression evaluated even when NDEBUG
is defined. Plus it’s helpful to print the error value and string in the unlikely event the function fails.
Such implementations can also be done for cnd_destroy
, mtx_destroy
, and tss_delete
.
The remaining functions require a bit more work.
mtx_init
Mutex initialization is handled slightly differently between the two APIs, specifically the way in which recursive mutexes are created, hence:
int mtx_init( mtx_t *m, int type ) {
pthread_mutexattr_t attr, *pattr = nullptr;
if ( (type & mtx_recursive) != 0 ) {
pthread_mutexattr_init( &attr );
pthread_mutexattr_settype( &attr, PTHREAD_MUTEX_RECURSIVE );
pattr = &attr;
}
int const mutex_init_rv = pthread_mutex_init( m, pattr );
if ( pattr != nullptr )
pthread_mutexattr_destroy( &pattr );
switch ( mutex_init_rv ) {
case 0 : return thrd_success;
case ENOMEM: return thrd_nomem;
default : return thrd_error;
}
}
thrd_sleep
One place where the standard API is not a subset of pthreads is that the former offers thrd_sleep
whereas the latter has no equivalent. However, POSIX has nanosleep
, hence:
int thrd_sleep( struct timespec const *duration,
struct timespec *remaining ) {
int const sleep_rv = nanosleep( duration, remaining );
switch ( sleep_rv ) {
case 0:
case -1:
return sleep_rv;
default:
return -2;
}
}
Creating & Joining Threads
The remaining two functions of thrd_create
and thrd_join
have a slightly different API that makes simple wrappers impossible. Specifically, in pthreads, the signature of the thread’s start function is void*(*)(void*)
(returns void*
) whereas in standard threads, the signature is int(*)(void*)
(returns int
). Why the difference? I don’t know. Regardless, we’ll need another data structure and function:
struct pthread_start_fn_data {
thrd_start_t start_fn; // C11 thread start function.
void *user_data; // Thread user data.
};
static void* pthread_start_fn_thunk( void *p ) {
struct pthread_start_fn_data const data =
*(struct pthread_start_fn_data*)p;
free( p );
return (void*)(intptr_t)data.start_fn( data.user_data );
}
int thrd_create( thrd_t *t, thrd_start_t start_fn, void *user_data ) {
struct pthread_start_fn_data *const data =
malloc( sizeof( struct pthread_start_fn_data ) );
*data = (struct pthread_start_fn_data){ start_fn, user_data };
int const create_rv = pthread_create(
t, /*attr=*/nullptr, &pthread_start_fn_thunk, data
);
if ( create_rv == 0 )
return thrd_success;
free( data );
return create_rv == EAGAIN ? thrd_nomem : thrd_error;
}
Since we want to call a thrd_start_t
that has a signature different from pthread’s, we’ll use pthread_start_fn_data
to remember the thrd_start_t
and the user data (if any), then give pthread_start_fn_thunk
to pthread_create
. Note that pthread_start_fn_data
must not be a local variable of thrd_create
since pthread_start_fn_thunk
might not start executing until after thrd_create
returns at which point local variables no longer exist. Hence, we’re forced to malloc
and free
it.
The implementation of thrd_join
has to convert the void*
returned by pthread_join
to the int
of thrd_join
:
int thrd_join( thrd_t t, int *pthrd_value ) {
void *pthread_value;
if ( pthread_join( t, &pthread_value ) != 0 )
return thrd_error;
if ( pthrd_value != nullptr )
*pthrd_value = (int)(intptr_t)pthread_value;
return thrd_success;
}
Conclusion
C’s standard threads’ API is (mostly) a subset of pthreads’ and is optional, even for conforming compilers. That’s problematic for software that needs to compile on many platforms. However, as demonstrated, you can implement a wrapper if you really want to use the standard API.
Epilogue: What about C++?
At around the same time as C11, C++11 also added standard library support for threads, but the C++ committee made it required, not optional like C. Of course that means you can rely standard threads being supported on all platforms that support C++. C++ standard threads’ API is also more fully featured than C’s. Hence if you’re programming in C++, choosing to use its standard threads is the reasonable choice.