POSIX threads cheatsheet from creating threads and protecting shared data to condition variables, thread pools, cancellation, and debugging.
pthread Cheatsheet
Section titled “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| Concept | API or Type | Purpose |
|---|---|---|
| Thread | pthread_t | Handle for one running thread |
| Mutex | pthread_mutex_t | Exclusive access to shared state |
| Condition variable | pthread_cond_t | Sleep until shared state may have changed |
| Read-write lock | pthread_rwlock_t | Many readers or one writer |
| One-time init | pthread_once_t | Initialize shared data exactly once |
| Thread-local data | pthread_key_t | Per-thread storage with optional cleanup |
pthreadis a POSIX API commonly available on Unix-like systems. It is not the native Windows threading API.
1. Beginner: Build and Create a Thread
Section titled “1. Beginner: Build and Create a Thread”Headers and Compile Command
Section titled “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 appUse -pthread, not only -lpthread: it lets the compiler and linker enable the platform’s required thread support.
Smallest Useful Program
Section titled “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; }}| Function | Meaning |
|---|---|
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
Section titled “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
Section titled “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
Section titled “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
Section titled “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| API | Use |
|---|---|
PTHREAD_MUTEX_INITIALIZER | Static initialization |
pthread_mutex_lock | Block until the mutex is acquired |
pthread_mutex_trylock | Attempt without waiting; may return EBUSY |
pthread_mutex_unlock | Release a mutex owned by this thread |
pthread_mutex_destroy | Clean up an unused mutex |
Mutex Rules
Section titled “Mutex Rules”- Associate each shared object with a clear mutex.
- Keep critical sections short; do not perform slow I/O while holding a lock unless required.
- Always acquire multiple locks in one consistent global order.
- Never destroy a locked mutex or one another thread may still access.
3. Wait for Work: Condition Variables
Section titled “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.
| API | Use |
|---|---|
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
Section titled “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 lockThis is the core of a thread pool: fixed worker threads, a synchronized job queue, and a shutdown flag.
4. Thread Lifetime and Shutdown
Section titled “4. Thread Lifetime and Shutdown”Joinable vs Detached
Section titled “Joinable vs Detached”| Mode | Choose When | Responsibility |
|---|---|---|
| Joinable, default | You need completion, result, or orderly shutdown | Call pthread_join exactly once |
| Detached | Fire-and-forget work truly owns all its resources | Never 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
Section titled “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
Section titled “5. Intermediate: Pick the Right Primitive”| Need | Primitive | Note |
|---|---|---|
| Protect an invariant or counter | Mutex | Default choice |
| Wait until work/state changes | Condition variable + mutex | Wait on a predicate |
| Mostly reads, longer protected operations | Read-write lock | Measure before replacing a mutex |
| Initialize shared global state once | pthread_once | Avoid racy lazy initialization |
| Per-thread context | Thread-specific data | Useful for library state |
| Synchronize phases among workers | Barrier | Not available on every POSIX-like platform |
Read-Write Lock
Section titled “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
Section titled “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
Section titled “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
Section titled “6. Advanced: Attributes, Cancellation, and Process Boundaries”Thread Attributes
Section titled “Thread Attributes”pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setstacksize(&attr, 1024 * 1024); // Check valid platform minimumspthread_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
Section titled “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;}| API | Meaning |
|---|---|
pthread_cancel(t) | Request cancellation of another thread |
pthread_setcancelstate | Enable or temporarily disable cancellation |
pthread_setcanceltype | Deferred is the practical safe default |
pthread_cleanup_push/pop | Register 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
Section titled “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
Section titled “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
Section titled “Common Failures”| Symptom | Likely Cause | Fix |
|---|---|---|
| Wrong or changing worker argument | Pointer to a reused loop variable | Store one argument object per worker |
| Program sometimes hangs | Deadlock or missed shutdown wakeup | Enforce lock order; broadcast after stop flag |
| High CPU while idle | Busy-waiting for work | Use a condition variable |
| Lost increments or corrupted data | Unprotected shared mutation | Use a mutex or suitable atomic operation |
| Thread resources grow | Joinable threads never joined | Join or detach every created thread |
| Slow scaling | Too much work under one lock | Reduce contention after measuring |
Helpful Build and Debug Commands
Section titled “Helpful Build and Debug Commands”cc -std=c17 -g -O1 -Wall -Wextra -pthread app.c -o appcc -std=c17 -g -O1 -fsanitize=thread -pthread app.c -o app-tsangdb ./appThreadSanitizer can reveal data races on supported compilers and platforms. Debuggers can inspect threads and backtraces:
info threadsthread apply all bt8. Quick API Reference
Section titled “8. Quick API Reference”| Category | Functions |
|---|---|
| Lifecycle | pthread_create, pthread_join, pthread_detach, pthread_exit |
| Identity | pthread_self, pthread_equal |
| Mutex | pthread_mutex_init, lock, trylock, unlock, destroy |
| Condition variable | pthread_cond_init, wait, timedwait, signal, broadcast, destroy |
| Read-write lock | pthread_rwlock_rdlock, wrlock, tryrdlock, trywrlock, unlock |
| Initialization | pthread_once |
| Thread-local state | pthread_key_create, pthread_getspecific, pthread_setspecific, pthread_key_delete |
| Configuration | pthread_attr_init, pthread_attr_setdetachstate, pthread_attr_setstacksize, pthread_attr_destroy |
| Cancellation | pthread_cancel, pthread_testcancel, pthread_cleanup_push, pthread_cleanup_pop |
Practical Checklist
Section titled “Practical Checklist”- Compile and link with
-pthread. - Define ownership and lifetime of each thread argument and result.
- Protect all shared mutable state with one documented synchronization strategy.
- Wait for predicates with condition variables in
whileloops. - Join or detach every successful thread creation.
- Prefer a bounded worker pool and cooperative shutdown for server-style work.
- Run a race detector while testing concurrent code.