The committee that standardizes the C programming language (ISO/IEC JTC1/SC22/WG14) has completed a major revision of the C standard. The previous version of the standard, completed in 1999, was colloquially known as "C99." As one might expect, the new revision completed at the very end of last year is known as "C11."

In this article and its companion article, I will describe the major new features of C11 in concurrency, security, and ease of use. A final article will discuss compatibility between C11 and C++.

Concurrency

C11 standardizes the semantics of multithreaded programs, potentially running on multicore platforms, and lightweight inter-thread communication using atomic variables.

The header <threads.h> provides macros, types, and functions to support multi-threading. Here is a summary of the macros, types, and enumeration constants:



Macros: thread_local, ONCE_FLAG, TSS_DTOR_ITERATIONS cnd_t, thrd_t, tss_t, mtx_t, tss_dtor_t, thrd_start_t, once_flag .



. Enumeration constants to pass to: mtx_init: mtx_plain, mtx_recursive, mtx_timed .

.

Enumeration constants for threads: thrd_timedout, thrd_success, thrd_busy, thrd_error, thrd_nomem .

.

Functions for condition variables:

call_once(once_flag *flag, void (*func)(void));

cnd_broadcast(cnd_t *cond);

cnd_destroy(cnd_t *cond);

cnd_init(cnd_t *cond);

cnd_signal(cnd_t *cond);

cnd_timedwait(cnd_t *restrict cond, mtx_t *restrict mtx, const struct timespec *restrict ts);

cnd_wait(cnd_t *cond, mtx_t *mtx);



The mutex functions:

void mtx_destroy(mtx_t *mtx);

int mtx_init(mtx_t *mtx, int type);

int mtx_lock(mtx_t *mtx);

int mtx_timedlock(mtx_t *restrict mtx,

const struct timespec *restrict ts);

int mtx_trylock(mtx_t *mtx);

int mtx_unlock(mtx_t *mtx);

functions:

Thread functions:

int thrd_create(thrd_t *thr, thrd_start_t func, void *arg);

thrd_t thrd_current(void);

int thrd_detach(thrd_t thr);

int thrd_equal(thrd_t thr0, thrd_t thr1);

noreturn void thrd_exit(int res);

int thrd_join(thrd_t thr, int *res);

int thrd_sleep(const struct timespec *duration, struct timespec *remaining);

void thrd_yield(void);



Thread-specific storage functions:

int tss_create(tss_t *key, tss_dtor_t dtor);

void tss_delete(tss_t key);

void *tss_get(tss_t key);

int tss_set(tss_t key, void *val);

These standardized library functions are more likely to be used as a foundation for easier-to-use APIs than as a platform for building applications. (See "When Tasks Replace Objects," by Andrew Binstock, for discussion of higher-level APIs.) For example, when using these low-level library functions it is very easy to create a data race, in which two or more threads write (or write-and-read) to the same location without synchronization. The C (and C++) standards permit any behavior if a data race happens to some variable x , which can lead to serious trouble. For example, some bytes of the value of x might be set by one thread and other bytes could be set by another thread ("torn values"), or some side-effect that appears to take place after assignment to x might (to another thread or another processor) appear to take place before that assignment. Here is a short program that contains an obvious data race, where the 64-bit integer ( long long ) named x is written and read by two threads:

#include <threads.h> #include <stdio.h> #define N 100000 char buf1[N][99]={0}, buf2[N][99]={0}; long long old1, old2, limit=N; long long x = 0; static void do1() { long long o1, o2, n1; for (long long i1 = 1; i1 < limit; ++i1) { old1 = x, x = i1; o1 = old1; o2 = old2; if (o1 > 0) { // x was set by this thread if (o1 != i1-1) sprintf(buf1[i1], "thread 1: o1=%7lld, i1=%7lld, o2=%7lld", o1, i1, o2); } else { // x was set by the other thread n1 = x, x = i1; if (n1 < 0 && n1 > o1) sprintf(buf1[i1], "thread 1: o1=%7lld, i1=%7lld, n1=%7lld", o1, i1, n1); } } } static void do2() { long long o1, o2, n2; for (long long i2 = -1; i2 > -limit; --i2) { old2 = x, x = i2; o1 = old1; o2 = old2; if (o2 < 0) { // x was set by this thread if (o2 != i2+1) sprintf(buf2[-i2], "thread 2: o2=%7lld, i2=%7lld, o1=%7lld", o2, i2, o1); } else { // x was set by the other thread n2 = x, x = i2; if (n2 > 0 && n2 < o2) sprintf(buf2[-i2], "thread 2: o2=%7lld, i2=%7lld, n2=%7lld", o2, i2, n2); } } } int main(int argc, char *argv[]) { thrd_t thr1; thrd_t thr2; thrd_create(&thr1, do1, 0); thrd_create(&thr2, do2, 0); thrd_join(&thr2, 0); thrd_join(&thr1, 0); for (long long i = 0; i < limit; ++i) { if (buf1[i][0] != '\0') printf("%s

", buf1[i]); if (buf2[i][0] != '\0') printf("%s

", buf2[i]); } return 0; }

If you had an implementation that already conformed to the C11 standard, and you compiled this program for a 32-bit machine (so that a 64-bit long long is written in two or more memory cycles), you could expect to see confirmation of the data race, with a varying number of lines of output such as this:

thread 2: o2=-4294947504, i2= -21, o1= 19792

The traditional solution for data races has been to create a lock. However, using atomic data can sometimes be more efficient. Loads and stores of atomic types are done with sequentially consistent semantics. In particular, if thread-1 stores a value in an atomic variable named x , and thread-2 reads that value, then all other stores previously performed in thread-1 (even to non-atomic objects) become visible to thread-2 . (The C11 and C++11 standards also provide other models of memory consistency, but even experts are cautioned to avoid them.)

The new header <stdatomic.h> provides a large set of named types and functions for the manipulation of atomic data. For example, atomic_llong is the typename provided for atomic long long integers. Similar names are provided for all the integer types. One of these typenames, atomic_flag , is required to be lock free. The standard includes a macro named ATOMIC_VAR_INIT(n) , for initializing atomic integers, as shown below.

The data race in the previous example can be cured by making x an atomic_llong variable. Simply change the one line that declares x in the aforementioned code sample:

#include <stdatomic.h> atomic_llong x = ATOMIC_VAR_INIT(0);

By using this atomic variable, the code operates without producing any data-race output.