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#
Types for
volatile,atomic_*, andsig_atomic_t.Errors for
errnoafter thread / process calls and signal handler restrictions.Control flow for
goto cleanuparound lock pairs.