Polyglot
Languages C concurrency
C § concurrency

Concurrency

C99 had no notion of concurrency. C11 introduced an optional threading library (<threads.h>), an optional atomic-operations library (<stdatomic.h>), and a memory model that gives meaning to inter-thread interactions. Implementations are not required to provide threads; a freestanding implementation may have neither. In practice, hosted implementations on Unix-like systems provide POSIX threads (<pthread.h>) almost universally, and the C11 thread API is implemented as a wrapper. Programs that need broad portability commonly use POSIX threads directly; programs that target only C11-conforming hosted systems may use the standard <threads.h> interface. This page covers both, alongside the atomic-operations and memory-model facilities that underlie any safe concurrency in C.

POSIX threads

POSIX threads (pthreads) are the de facto threading API on Unix-like systems. The principal operations:

#include <pthread.h>
#include <stdio.h>

void *worker(void *arg) {
    int id = *(int *)arg;
    printf("thread %d running\n", id);
    return NULL;
}

int main(void) {
    pthread_t threads[4];
    int       ids[4] = {0, 1, 2, 3};

    for (int i = 0; i < 4; ++i) {
        pthread_create(&threads[i], NULL, worker, &ids[i]);
    }
    for (int i = 0; i < 4; ++i) {
        pthread_join(threads[i], NULL);
    }
    return 0;
}

pthread_create starts a new thread running the supplied function with the supplied argument; the function’s return value is conveyed through pthread_join, which blocks until the thread finishes. A thread may be detached with pthread_detach, after which it cannot be joined and its resources are reclaimed automatically when it exits.

The pthread library provides:

  • Thread managementpthread_create, pthread_join, pthread_detach, pthread_self, pthread_exit, pthread_cancel.
  • Mutexespthread_mutex_init, pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock, pthread_mutex_destroy. Variations exist for recursive and timed locking.
  • Condition variablespthread_cond_init, pthread_cond_wait, pthread_cond_signal, pthread_cond_broadcast.
  • Read-write lockspthread_rwlock_* for the multiple-reader, single-writer pattern.
  • Once-initialisationpthread_once for thread-safe one-time initialisation.

The pthread library is large; this section covers only the essentials.

C11 <threads.h>

C11’s thread API is structurally similar to pthreads, with renamed functions and a slightly different return-value convention:

#include <threads.h>
#include <stdio.h>

int worker(void *arg) {
    int id = *(int *)arg;
    printf("thread %d running\n", id);
    return 0;
}

int main(void) {
    thrd_t threads[4];
    int    ids[4] = {0, 1, 2, 3};

    for (int i = 0; i < 4; ++i) {
        thrd_create(&threads[i], worker, &ids[i]);
    }
    for (int i = 0; i < 4; ++i) {
        thrd_join(threads[i], NULL);
    }
    return 0;
}

The library is optional: an implementation that does not provide threads defines __STDC_NO_THREADS__. Conforming code that uses <threads.h> checks for the macro and provides an alternative path or a build-time error.

The principal types:

  • thrd_t — a thread.
  • mtx_t — a mutex (with flavours mtx_plain, mtx_recursive, mtx_timed).
  • cnd_t — a condition variable.
  • tss_t — thread-specific storage.
  • once_flag — for one-time initialisation, with call_once.

The corresponding functions follow naming conventions: thrd_*, mtx_*, cnd_*, tss_*. Adoption is uneven; pthreads remain more widely used.

Mutexes

A mutex (mutual-exclusion lock) protects a critical section: only one thread may hold the lock at a time. The conventional pattern:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int             counter = 0;

void increment(void) {
    pthread_mutex_lock(&lock);
    ++counter;
    pthread_mutex_unlock(&lock);
}

Every access to a shared variable that may be modified concurrently must be protected by the same mutex; the discipline is the responsibility of the programmer. A read that is not protected, or that is protected by a different mutex, is a data race — concurrent unsynchronised access at least one of which is a write — and produces undefined behaviour under the C memory model.

The recurring discipline:

  • Lock once, unlock once. Every lock is paired with exactly one unlock on every path through the function.
  • Hold for the minimum. Critical sections should be short. Long-held locks reduce concurrency; locks held during a wait can deadlock.
  • Acquire in a fixed order. When more than one mutex is held simultaneously, all code paths must acquire them in the same order. Inconsistent ordering produces deadlock.

The cleanup-label idiom from Error handling extends naturally to lock-holding functions: the cleanup label unlocks any locks that were taken on the success path.

Condition variables

A condition variable lets a thread wait until another thread signals that some condition has become true. The condition itself is checked by the waiter; the condition variable provides the signal:

pthread_mutex_t lock  = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  ready = PTHREAD_COND_INITIALIZER;
int             data_available = 0;

void *consumer(void *arg) {
    pthread_mutex_lock(&lock);
    while (!data_available) {           /* always loop, never just if */
        pthread_cond_wait(&ready, &lock);
    }
    /* handle data; we hold the lock here */
    pthread_mutex_unlock(&lock);
    return NULL;
}

void producer_signal(void) {
    pthread_mutex_lock(&lock);
    data_available = 1;
    pthread_cond_signal(&ready);
    pthread_mutex_unlock(&lock);
}

The convention to test the condition in a while loop is essential. pthread_cond_wait may return spuriously — without the condition having been signalled — and may also return when the condition was signalled but another thread has invalidated it before the waiter ran. The loop is the defence: re-test, and wait again if the condition is not actually true.

Atomic operations

<stdatomic.h> (C11) provides operations that are guaranteed to be indivisible: a load, a store, or a read-modify-write that no other thread can observe in a partially-completed state. The principal types are _Atomic-qualified versions of the integer types and the atomic_* typedefs; the principal operations are atomic load, store, exchange, compare-and-swap, and arithmetic.

#include <stdatomic.h>
#include <stdint.h>

atomic_int counter = 0;

void increment(void) {
    atomic_fetch_add(&counter, 1);
}

int read_count(void) {
    return atomic_load(&counter);
}

Atomic operations admit several memory orderingsmemory_order_relaxed, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst — that govern how the operation interacts with surrounding memory accesses. The default for non-suffixed operations (atomic_load, atomic_store) is memory_order_seq_cst, the strongest and slowest. The relaxed forms (atomic_load_explicit(&x, memory_order_relaxed)) trade ordering guarantees for performance and are appropriate only when the program has no inter-thread dependency through that variable.

The compare-and-swap primitive — atomic comparison with a value and exchange if the comparison succeeds — is the foundation of most lock-free data structures:

atomic_int  state = STATE_IDLE;
int         expected = STATE_IDLE;
if (atomic_compare_exchange_strong(&state, &expected, STATE_RUNNING)) {
    /* successfully transitioned IDLE -> RUNNING */
} else {
    /* state was not IDLE; the actual value is now in `expected` */
}

Lock-free programming in C is feasible with <stdatomic.h> but is not a conventional first choice: the correctness conditions are subtle, the testing is difficult, and the performance benefit over carefully-used locks is often modest.

The C memory model

C11 defines a memory model — a specification of which sequences of memory operations across threads are well-defined and what the program may observe. The principal rules:

  • Sequential consistency for data-race-free programs. A program with no data races behaves as if all of its operations were interleaved sequentially across threads. The implementation may reorder operations within a thread, but the externally observable behaviour is consistent with some sequential interleaving.
  • Data races are undefined behaviour. Two unsynchronised concurrent accesses to the same memory location, at least one of which is a write, is a data race. The standard places no constraint on the resulting behaviour.
  • Synchronisation establishes happens-before. Mutex lock/unlock pairs, atomic acquire/release operations, and thread creation/join establish ordering relations. Operations before a release on one thread are visible after the matching acquire on another.

The three principal mechanisms for safe inter-thread communication, in order of decreasing weight:

  1. Mutexes — the simplest model: everything inside the lock is sequentially consistent with everything else inside the same lock. Most concurrent code uses mutexes.
  2. Atomics with sequential consistency — for shared counters, flags, and other small state. Cheaper than mutexes for simple operations but still expensive on most architectures.
  3. Atomics with relaxed ordering — for performance-critical paths where the program structure makes the ordering implicit. Difficult to use correctly.

Thread-local storage

Variables marked _Thread_local (C11) or thread_local (C23) have one instance per thread:

_Thread_local int last_error = 0;

Thread-local objects are private to their thread; access does not require synchronisation. The principal use is per-thread caches and error states (errno itself is conceptually _Thread_local on conforming C11 implementations). The mechanism has implementation costs — extra setup at thread creation, and per-platform restrictions on what types may be made thread-local — but is the cleanest answer when per-thread state is genuinely needed.

POSIX has an older API for thread-specific storage — pthread_key_create, pthread_setspecific, pthread_getspecific — that achieves the same effect with explicit setup. The pthread API is more flexible (it supports a destructor at thread exit) at the cost of more boilerplate.

Signals

POSIX signals are an asynchronous notification mechanism: a process or thread can be sent a signal that interrupts its execution and runs a handler function. The mechanism interacts with concurrency in non-trivial ways:

  • A signal handler can run on any thread that has not blocked the signal; the thread that receives the signal is implementation-defined.
  • A signal handler is restricted to async-signal-safe operations — a small set of functions that can be called from a signal handler without producing undefined behaviour. printf, malloc, and most of the standard library are not async-signal-safe.
  • Communication between a signal handler and the rest of the program must be through volatile sig_atomic_t variables or through atomic operations of suitable lock-free guarantee.

Conventional contemporary practice limits signal handlers to setting a flag that the main loop polls, with all real work happening in the main loop. The treatment of signals is properly the subject of POSIX, not C; the standard’s coverage is intentionally limited.

A note on choosing concurrency primitives

The typical progression for new C concurrency code:

  1. Avoid concurrency where possible. A single-threaded program is far easier to reason about. Many “concurrent” workloads are better expressed as a single thread with non-blocking I/O.
  2. Use thread pools and queues. When concurrency is necessary, the conventional structure is a pool of worker threads consuming a queue of work items. The shared state is the queue; the workers are otherwise independent.
  3. Use mutexes for shared state. The mutex-protected critical section is well-understood, easy to reason about, and reasonably performant for most uses.
  4. Use atomics for fine-grained state. Counters, flags, and the queue’s enqueue/dequeue counters benefit from atomic operations; full mutex-protection is unnecessary overhead.
  5. Use lock-free structures only where measurement justifies it. Lock-free implementations are subtle, difficult to test, and only sometimes outperform well-designed mutex-protected alternatives.

The progression matches the conventional advice in concurrency literature: prefer simpler primitives, complicate only when measurement demands it, and accept the modest performance loss of straightforward designs in exchange for code that the programmer can reason about and the reviewer can verify.