Frontend Labs
⋯

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(&not_full, &mtx);  // sleep until there's room
    buf[tail] = value; tail = (tail + 1) % CAP; count++;
    pthread_cond_signal(&not_empty);                          // wake a waiting consumer
    pthread_mutex_unlock(&mtx);
}

int consume(void) {
    pthread_mutex_lock(&mtx);
    while (count == 0) pthread_cond_wait(&not_empty, &mtx);   // sleep until there's data
    int value = buf[head]; head = (head + 1) % CAP; count--;
    pthread_cond_signal(&not_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.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36