Skip to content

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

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.


#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
Terminal window
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.

#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

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.

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.


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;
}
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
  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.

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
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.


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);

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.


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
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);
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 */
}
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”
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.

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.

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.


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);
}
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
Terminal window
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

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
  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.