Neural Architecture Hub
C & C++

pthread

POSIX threads cheatsheet from creating threads and protecting shared data to condition variables, thread pools, cancellation, and debugging.

pthread Cheatsheet

POSIX threads (pthread) let one process execute multiple flows concurrently while sharing memory and open resources. Use threads for independent work or blocking tasks; synchronize every shared mutable object.

create -> run concurrently -> coordinate shared state -> join or detach
ConceptAPI or TypePurpose
Threadpthread_tHandle for one running thread
Mutexpthread_mutex_tExclusive access to shared state
Condition variablepthread_cond_tSleep until shared state may have changed
Read-write lockpthread_rwlock_tMany readers or one writer
One-time initpthread_once_tInitialize shared data exactly once
Thread-local datapthread_key_tPer-thread storage with optional cleanup

pthread is a POSIX API commonly available on Unix-like systems. It is not the native Windows threading API.


1. Beginner: Build and Create a Thread

Headers and Compile Command

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
cc -std=c17 -Wall -Wextra -Wpedantic -pthread app.c -o app

Use -pthread, not only -lpthread: it lets the compiler and linker enable the platform's required thread support.

Smallest Useful Program

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

struct task {
    int id;
    const char *message;
};

static void *worker(void *arg)
{
    struct task *task = arg;
    printf("worker %d: %s\n", task->id, task->message);
    return NULL;
}

int main(void)
{
    pthread_t thread;
    struct task task = { .id = 1, .message = "hello" };

    int err = pthread_create(&thread, NULL, worker, &task);
    if (err != 0) {
        fprintf(stderr, "pthread_create: %s\n", strerror(err));
        return 1;
    }

    err = pthread_join(thread, NULL);
    if (err != 0) {
        fprintf(stderr, "pthread_join: %s\n", strerror(err));
        return 1;
    }
}
FunctionMeaning
pthread_create(&t, NULL, fn, arg)Start fn(arg) in a new thread
pthread_join(t, &result)Wait for a joinable thread and collect its result
pthread_detach(t)Release its resources automatically when it exits
pthread_self()Obtain the current thread ID
pthread_equal(a, b)Compare two thread IDs
pthread_exit(value)Finish the calling thread with a result

Pass Arguments Safely

The pointer passed to pthread_create must remain valid until the worker is done using it.

struct task tasks[4];
pthread_t threads[4];

for (int i = 0; i < 4; ++i) {
    tasks[i].id = i;                    // One stable object per worker
    tasks[i].message = "work";
    pthread_create(&threads[i], NULL, worker, &tasks[i]);
}

for (int i = 0; i < 4; ++i) {
    pthread_join(threads[i], NULL);
}

Avoid passing &i from the loop: workers would share one changing variable.

Returning a Value

static void *calculate(void *arg)
{
    int input = *(int *)arg;
    int *answer = malloc(sizeof *answer);
    if (answer != NULL) {
        *answer = input * 2;
    }
    return answer;
}

void *result = NULL;
pthread_join(thread, &result);
int *answer = result;
if (answer != NULL) {
    printf("%d\n", *answer);
    free(answer);
}

Do not return a pointer to a worker's local variable; its lifetime ends when the function returns.


2. Shared Data: Mutexes

Reading or writing shared mutable data without synchronization creates a data race and invalid program behavior.

static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static long counter = 0;

static void *increment(void *unused)
{
    (void)unused;

    for (int i = 0; i < 100000; ++i) {
        pthread_mutex_lock(&lock);
        ++counter;
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

Lifecycle and Operations

pthread_mutex_t lock;

pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);
/* read or modify protected state */
pthread_mutex_unlock(&lock);
pthread_mutex_destroy(&lock);   // Only after no thread can use it
APIUse
PTHREAD_MUTEX_INITIALIZERStatic initialization
pthread_mutex_lockBlock until the mutex is acquired
pthread_mutex_trylockAttempt without waiting; may return EBUSY
pthread_mutex_unlockRelease a mutex owned by this thread
pthread_mutex_destroyClean up an unused mutex

Mutex Rules

  1. Associate each shared object with a clear mutex.
  2. Keep critical sections short; do not perform slow I/O while holding a lock unless required.
  3. Always acquire multiple locks in one consistent global order.
  4. Never destroy a locked mutex or one another thread may still access.

3. Wait for Work: Condition Variables

A condition variable avoids busy-waiting. It is paired with a mutex and a predicate, such as ready != 0 or queue_size > 0.

static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t changed = PTHREAD_COND_INITIALIZER;
static int ready = 0;

static void *consumer(void *unused)
{
    (void)unused;

    pthread_mutex_lock(&lock);
    while (!ready) {
        pthread_cond_wait(&changed, &lock);
    }
    puts("data is ready");
    pthread_mutex_unlock(&lock);
    return NULL;
}

static void publish(void)
{
    pthread_mutex_lock(&lock);
    ready = 1;
    pthread_cond_signal(&changed);
    pthread_mutex_unlock(&lock);
}

pthread_cond_wait atomically releases the mutex while sleeping and reacquires it before returning. Always check the predicate in a while loop because wakes can be spurious or another consumer may take the work first.

APIUse
pthread_cond_wait(&cv, &lock)Wait indefinitely for a state change
pthread_cond_timedwait(&cv, &lock, &deadline)Wait until an absolute deadline
pthread_cond_signal(&cv)Wake at least one waiter
pthread_cond_broadcast(&cv)Wake all waiters

Producer/Consumer Skeleton

pthread_mutex_lock(&queue.lock);
while (queue.empty && !queue.stop) {
    pthread_cond_wait(&queue.has_work, &queue.lock);
}
if (queue.stop && queue.empty) {
    pthread_mutex_unlock(&queue.lock);
    return NULL;
}
struct job job = pop_job(&queue);
pthread_mutex_unlock(&queue.lock);

run_job(job);                 // Work outside the lock

This is the core of a thread pool: fixed worker threads, a synchronized job queue, and a shutdown flag.


4. Thread Lifetime and Shutdown

Joinable vs Detached

ModeChoose WhenResponsibility
Joinable, defaultYou need completion, result, or orderly shutdownCall pthread_join exactly once
DetachedFire-and-forget work truly owns all its resourcesNever join it
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&thread, &attr, worker, arg);
pthread_attr_destroy(&attr);

Cooperative Shutdown

Prefer setting a protected stop flag and waking sleepers over abruptly canceling threads.

pthread_mutex_lock(&queue.lock);
queue.stop = 1;
pthread_cond_broadcast(&queue.has_work);
pthread_mutex_unlock(&queue.lock);

for (size_t i = 0; i < worker_count; ++i) {
    pthread_join(workers[i], NULL);
}

This lets workers release locks, close files, and finish ownership cleanup predictably.


5. Intermediate: Pick the Right Primitive

NeedPrimitiveNote
Protect an invariant or counterMutexDefault choice
Wait until work/state changesCondition variable + mutexWait on a predicate
Mostly reads, longer protected operationsRead-write lockMeasure before replacing a mutex
Initialize shared global state oncepthread_onceAvoid racy lazy initialization
Per-thread contextThread-specific dataUseful for library state
Synchronize phases among workersBarrierNot available on every POSIX-like platform

Read-Write Lock

static pthread_rwlock_t config_lock = PTHREAD_RWLOCK_INITIALIZER;

pthread_rwlock_rdlock(&config_lock);
/* read shared configuration */
pthread_rwlock_unlock(&config_lock);

pthread_rwlock_wrlock(&config_lock);
/* replace shared configuration */
pthread_rwlock_unlock(&config_lock);

Initialize Once

static pthread_once_t once = PTHREAD_ONCE_INIT;

static void initialize_library(void)
{
    /* set up immutable shared state */
}

static void use_library(void)
{
    pthread_once(&once, initialize_library);
    /* initialization is complete here */
}

Thread-Local Storage

static pthread_key_t key;
static pthread_once_t key_once = PTHREAD_ONCE_INIT;

static void make_key(void)
{
    pthread_key_create(&key, free);
}

static char *thread_buffer(void)
{
    pthread_once(&key_once, make_key);

    char *buf = pthread_getspecific(key);
    if (buf == NULL) {
        buf = calloc(128, 1);
        pthread_setspecific(key, buf);
    }
    return buf;
}

The key destructor runs for a non-NULL value when the thread exits.


6. Advanced: Attributes, Cancellation, and Process Boundaries

Thread Attributes

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 1024 * 1024);  // Check valid platform minimums
pthread_create(&thread, &attr, worker, arg);
pthread_attr_destroy(&attr);

Use custom stack sizes only after measuring or when large recursion/local storage makes it necessary. Scheduling attributes and priorities may require privileges and platform-specific policy.

Cancellation

static void unlock(void *arg)
{
    pthread_mutex_unlock(arg);
}

static void *cancelable_worker(void *unused)
{
    (void)unused;

    pthread_mutex_lock(&lock);
    pthread_cleanup_push(unlock, &lock);
    while (!ready) {
        pthread_cond_wait(&changed, &lock);  // Cancellation point
    }
    pthread_cleanup_pop(1);                  // Unlock on normal path too
    return NULL;
}
APIMeaning
pthread_cancel(t)Request cancellation of another thread
pthread_setcancelstateEnable or temporarily disable cancellation
pthread_setcanceltypeDeferred is the practical safe default
pthread_cleanup_push/popRegister cleanup for cancellation or normal exit
pthread_testcancel()Explicit deferred cancellation point

Cancellation is subtle around ownership of locks and resources. Prefer cooperative shutdown unless cancellation is part of a carefully designed contract.

fork() in a Multithreaded Process

After fork(), only the calling thread exists in the child; locks formerly held by other threads can remain unusable there. In the child, generally call only async-signal-safe functions until exec(). pthread_atfork can prepare and repair application-owned lock state when unavoidable.


7. Errors, Debugging, and Performance

Most pthread functions return an error number directly; they do not report failure through errno.

int err = pthread_mutex_lock(&lock);
if (err != 0) {
    fprintf(stderr, "pthread_mutex_lock: %s\n", strerror(err));
    exit(EXIT_FAILURE);
}

Common Failures

SymptomLikely CauseFix
Wrong or changing worker argumentPointer to a reused loop variableStore one argument object per worker
Program sometimes hangsDeadlock or missed shutdown wakeupEnforce lock order; broadcast after stop flag
High CPU while idleBusy-waiting for workUse a condition variable
Lost increments or corrupted dataUnprotected shared mutationUse a mutex or suitable atomic operation
Thread resources growJoinable threads never joinedJoin or detach every created thread
Slow scalingToo much work under one lockReduce contention after measuring

Helpful Build and Debug Commands

cc -std=c17 -g -O1 -Wall -Wextra -pthread app.c -o app
cc -std=c17 -g -O1 -fsanitize=thread -pthread app.c -o app-tsan
gdb ./app

ThreadSanitizer can reveal data races on supported compilers and platforms. Debuggers can inspect threads and backtraces:

info threads
thread apply all bt

8. Quick API Reference

CategoryFunctions
Lifecyclepthread_create, pthread_join, pthread_detach, pthread_exit
Identitypthread_self, pthread_equal
Mutexpthread_mutex_init, lock, trylock, unlock, destroy
Condition variablepthread_cond_init, wait, timedwait, signal, broadcast, destroy
Read-write lockpthread_rwlock_rdlock, wrlock, tryrdlock, trywrlock, unlock
Initializationpthread_once
Thread-local statepthread_key_create, pthread_getspecific, pthread_setspecific, pthread_key_delete
Configurationpthread_attr_init, pthread_attr_setdetachstate, pthread_attr_setstacksize, pthread_attr_destroy
Cancellationpthread_cancel, pthread_testcancel, pthread_cleanup_push, pthread_cleanup_pop

Practical Checklist

  1. Compile and link with -pthread.
  2. Define ownership and lifetime of each thread argument and result.
  3. Protect all shared mutable state with one documented synchronization strategy.
  4. Wait for predicates with condition variables in while loops.
  5. Join or detach every successful thread creation.
  6. Prefer a bounded worker pool and cooperative shutdown for server-style work.
  7. Run a race detector while testing concurrent code.

On this page