Concurrency#

C ships two concurrency surfaces. POSIX threads (pthread_* from <pthread.h>) is the historical and most-deployed API; C11 threads (<threads.h>, also thrd_*) is the standard alternative the operator sees less often. Atomics live in <stdatomic.h> (C11+). Process-level concurrency is fork / exec / signals from <unistd.h>.

/types`. For signal handlers, see Errors.

pthreads#

Creating a thread.

#include <pthread.h>

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

int main(void) {
    pthread_t t;
    int id = 7;
    if (pthread_create(&t, NULL, worker, &id) != 0) {
        perror("pthread_create");
        return 1;
    }
    pthread_join(t, NULL);            /* wait for it to finish */
    return 0;
}

Compile with -pthread.

$ cc -pthread -o app main.c

Mutexes#

A mutex serialises access to shared mutable state.

typedef struct {
    pthread_mutex_t mu;
    int             n;
} counter_t;

void counter_init(counter_t *c) {
    pthread_mutex_init(&c->mu, NULL);
    c->n = 0;
}

void counter_inc(counter_t *c) {
    pthread_mutex_lock(&c->mu);
    c->n++;
    pthread_mutex_unlock(&c->mu);
}

int counter_get(counter_t *c) {
    pthread_mutex_lock(&c->mu);
    int v = c->n;
    pthread_mutex_unlock(&c->mu);
    return v;
}

For static initialisation (no constructor call needed), PTHREAD_MUTEX_INITIALIZER.

static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;

The operator pairs every lock with an unlock on every exit path, including error returns. goto cleanup (see Control flow) is the standard tool.

Condition variables#

For wait-until-signalled patterns.

pthread_mutex_t  mu = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t   cv = PTHREAD_COND_INITIALIZER;
int              ready = 0;

/* consumer */
pthread_mutex_lock(&mu);
while (!ready) pthread_cond_wait(&cv, &mu);    /* loop, not if */
/* … */
pthread_mutex_unlock(&mu);

/* producer */
pthread_mutex_lock(&mu);
ready = 1;
pthread_cond_signal(&cv);                       /* or broadcast */
pthread_mutex_unlock(&mu);

The while (not if) around pthread_cond_wait guards against spurious wake-ups; cond_wait can return without a signal.

Read / write locks#

pthread_rwlock_t for many-readers, one-writer patterns.

pthread_rwlock_t rw = PTHREAD_RWLOCK_INITIALIZER;
pthread_rwlock_rdlock(&rw); /* shared read */
/* … */
pthread_rwlock_unlock(&rw);

pthread_rwlock_wrlock(&rw); /* exclusive write */
/* … */
pthread_rwlock_unlock(&rw);

Thread-local storage#

C11’s thread_local (_Thread_local before C23) gives one storage slot per thread.

#include <threads.h>

thread_local int per_thread = 0;

Use this for per-thread caches and to avoid mutexes on hot paths.

Atomics#

<stdatomic.h> exposes lock-free primitives for counters, flags, and pointer swaps. Cheap; mandatory for cross-thread single-word state.

#include <stdatomic.h>

atomic_int counter = 0;
atomic_fetch_add(&counter, 1);
int v = atomic_load(&counter);
atomic_store(&counter, 0);

atomic_bool stop = false;
atomic_store(&stop, true);

Memory order. The default memory_order_seq_cst is the safest; pick weaker orders (memory_order_relaxed, acquire, release) only for proven hot paths.

C11 threads#

<threads.h> is the standard C alternative; thinner than pthreads, less deployed.

#include <threads.h>

int worker(void *arg) { return 0; }

thrd_t t;
thrd_create(&t, worker, NULL);
thrd_join(t, NULL);

mtx_t  m; mtx_init(&m, mtx_plain);
mtx_lock(&m); /* … */ mtx_unlock(&m);

Most production code still uses pthreads because of ecosystem support.

fork and exec#

Process-level concurrency: fork creates a child process, exec replaces the current process image, waitpid joins.

#include <unistd.h>
#include <sys/wait.h>

pid_t pid = fork();
if (pid < 0) { perror("fork"); return -1; }

if (pid == 0) {                       /* child */
    execlp("ls", "ls", "-la", (char*)NULL);
    perror("execlp");                  /* only on failure */
    _exit(127);
}

/* parent */
int status;
waitpid(pid, &status, 0);

The classic Unix pattern. Child uses _exit (not exit) so atexit handlers do not double-run.

Pipes and IPC#

pipe(fd) creates a one-way IPC channel; the operator pairs it with fork for parent / child communication.

int fds[2];
pipe(fds);                              /* fds[0] read, fds[1] write */

if (fork() == 0) {
    close(fds[0]);
    write(fds[1], "hi", 2);
    close(fds[1]);
    _exit(0);
}
close(fds[1]);
char buf[16];
ssize_t n = read(fds[0], buf, sizeof buf);

For higher-level IPC: shared memory (shm_open / mmap), message queues (POSIX mq_*), Unix-domain sockets (socket AF_UNIX).

Signals#

Signal handlers run asynchronously; only async-signal-safe functions are legal inside. volatile sig_atomic_t is the only type you should write to from a handler.

#include <signal.h>

volatile sig_atomic_t stop = 0;
static void on_sigint(int sig) { stop = 1; }

struct sigaction sa = {.sa_handler = on_sigint};
sigaction(SIGINT, &sa, NULL);

while (!stop) work();

See Errors for the safe-function rules.

References#