20. Concurrency: POSIX threads, mutexes/condvars, C11 <threads.h> and atomics
20.1 Threads and the race condition they immediately create
#include <pthread.h>
#include <stdio.h>
static int counter = 0; // shared, unprotected — this is the bug
void *increment(void *arg) {
(void)arg;
for (int i = 0; i < 100000; i++) counter++; // read-modify-write, not atomic
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%d\n", counter); // almost never 200000 — a classic data race
return 0;
}
Compile with -fsanitize=thread to have ThreadSanitizer point at the
exact racing lines instead of guessing from a wrong number.
20.2 The fix: a mutex
#include <pthread.h>
#include <stdio.h>
static int counter = 0;
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *increment(void *arg) {
(void)arg;
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&lock);
counter++; // now protected — only one thread inside at a time
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%d\n", counter); // always 200000
return 0;
}
20.3 Producer/consumer with a condition variable
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#define CAP 5
static int buf[CAP];
static int head = 0, tail = 0, count = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
static pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void produce(int value) {
pthread_mutex_lock(&mtx);
while (count == CAP) pthread_cond_wait(¬_full, &mtx); // sleep until there's room
buf[tail] = value; tail = (tail + 1) % CAP; count++;
pthread_cond_signal(¬_empty); // wake a waiting consumer
pthread_mutex_unlock(&mtx);
}
int consume(void) {
pthread_mutex_lock(&mtx);
while (count == 0) pthread_cond_wait(¬_empty, &mtx); // sleep until there's data
int value = buf[head]; head = (head + 1) % CAP; count--;
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mtx);
return value;
}
pthread_cond_wait atomically unlocks the mutex and sleeps, then
re-locks it before returning — this is why it takes the mutex as an
argument, and it's the exact primitive behind every blocking queue in
every language runtime (Java's wait/notify, Python's
threading.Condition).
20.4 C11 atomics: lock-free counters
#include <stdatomic.h>
#include <pthread.h>
#include <stdio.h>
static atomic_int counter = 0;
void *increment(void *arg) {
(void)arg;
for (int i = 0; i < 100000; i++) {
atomic_fetch_add(&counter, 1); // one indivisible hardware instruction, no mutex
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%d\n", atomic_load(&counter)); // always 200000, no lock needed
return 0;
}
Atomics are faster than a mutex for simple counters (no syscall, no
thread blocking) but they only protect one memory location at a time —
the moment an invariant spans two variables, you need a real lock.
Section 38 covers the memory-ordering semantics (memory_order_relaxed
vs. acquire/release vs. seq_cst) that atomic_fetch_add's default
sequential consistency hides from you here.