PThreads

Overview

PThreads = POSIX Threads — the de facto multithreading standard on UNIX systems. Part of the POSIX (Portable Operating Systems Interface) specification. Covers thread creation/management and synchronization primitives (mutexes, condition variables).

Thread Creation

Data types and functions (mapped to Birrell’s model):

  • pthread_t — thread handle (ID, execution state, internal metadata)

  • pthread_create(thread, attr, start_routine, arg) — creates thread executing start_routine(arg); returns status (analogous to Birrell’s Fork)

  • pthread_join(thread, status) — blocks until target thread completes; retrieves return value (analogous to Birrell’s Join)

Thread Attributes

pthread_attr_t passed to pthread_create controls:

  • Stack size

  • Scheduling policy/priority

  • Scope: system or process

  • Joinable vs detached state

  • Inheritance from calling thread

Pass NULL for defaults. Attribute management functions:

  • pthread_attr_init() / pthread_attr_destroy()

  • pthread_attr_set*() / pthread_attr_get*()

Detachable Threads

  • Joinable (default): parent must join children; parent exiting before join → zombie threads

  • Detached: cannot be joined; parent and children independent; parent can exit freely via pthread_exit()

  • Detach via pthread_detach(thread) or create with PTHREAD_CREATE_DETACHED attribute

Example attribute setup:

pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&thread, &attr, routine, arg);

Compiling PThreads

  • Include #include <pthread.h>

  • Link with -lpthread or use -pthread flag (preferred — also configures compilation for threads)

  • Always check return values of pthread functions

Thread Creation Examples

Basic creation and join:

void *hello(void *arg) {
    printf("Hello Thread\n");
    return NULL;
}

int main() {
    pthread_t threads[4];
    for (int i = 0; i < 4; i++)
        pthread_create(&threads[i], NULL, hello, NULL);
    for (int i = 0; i < 4; i++)
        pthread_join(threads[i], NULL);
}

Output: “Hello Thread” printed 4 times (order may vary).

Race condition with shared argument:

void *thread_func(void *arg) {
    int *p = (int *)arg;
    int myNum = *p;
    printf("Thread number %d\n", myNum);
    return NULL;
}

int main() {
    pthread_t threads[4];
    for (int i = 0; i < 4; i++)
        pthread_create(&threads[i], NULL, thread_func, &i);  // BUG: race on &i
    ...
}

Passing &i shares the same variable across all threads — a thread may read i after main increments it → data race.

Fix: give each thread its own copy of the argument:

int tNum[4];
for (int i = 0; i < 4; i++) {
    tNum[i] = i;
    pthread_create(&threads[i], NULL, thread_func, &tNum[i]);
}

PThread Mutexes

  • pthread_mutex_t — mutex type

  • pthread_mutex_lock(mutex) — acquire (blocks if held)

  • pthread_mutex_unlock(mutex) — release

  • pthread_mutex_init(mutex, attr) — initialize; NULL attr = default (process-private)

  • pthread_mutex_trylock(mutex) — non-blocking: returns immediately if mutex held (allows thread to do other work)

  • pthread_mutex_destroy(mutex) — free resources

Example — safe_insert:

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;

void safe_insert(int val) {
    pthread_mutex_lock(&m);
    insert(my_list, val);
    pthread_mutex_unlock(&m);
}

Mutex attributes: can set process-shared (cross-process visibility) vs process-private (default).

Best practices:

  • Always protect shared data through a single mutex

  • Declare mutexes as global variables (visible to all threads)

  • Establish and enforce a global lock order to prevent deadlocks

  • Always unlock the correct mutex; don’t forget to unlock

PThread Condition Variables

  • pthread_cond_t — condition variable type

  • pthread_cond_wait(cond, mutex) — atomically release mutex + block; on wakeup re-acquire mutex

  • pthread_cond_signal(cond) — wake one waiter

  • pthread_cond_broadcast(cond) — wake all waiters

  • pthread_cond_init(cond, attr) / pthread_cond_destroy(cond)

Semantics identical to Birrell’s Wait/Signal/Broadcast.

Best practices:

  • Always signal/broadcast when predicate changes

  • Use broadcast when unsure (safe but slower); use signal when exactly one waiter should proceed

  • Signal/broadcast can be moved after unlock if not conditional on shared state (avoids spurious wakeups)

Producer-Consumer Example

Shared state:

#define BUFFER_SIZE 3
int buffer[BUFFER_SIZE];
int num = 0, add = 0, rem = 0;

pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c_cons;  // consumers wait here
pthread_cond_t c_prod;  // producers wait here

Producer:

void *producer(void *arg) {
    for (int i = 0; i < 20; i++) {
        pthread_mutex_lock(&m);
        while (num == BUFFER_SIZE)
            pthread_cond_wait(&c_prod, &m);
        buffer[add] = i;
        add = (add + 1) % BUFFER_SIZE;
        num++;
        pthread_mutex_unlock(&m);
        pthread_cond_signal(&c_cons);
    }
    return NULL;
}

Consumer:

void *consumer(void *arg) {
    while (1) {
        pthread_mutex_lock(&m);
        while (num == 0)
            pthread_cond_wait(&c_cons, &m);
        int val = buffer[rem];
        rem = (rem + 1) % BUFFER_SIZE;
        num--;
        pthread_mutex_unlock(&m);
        pthread_cond_signal(&c_prod);
        printf("Consumed: %d\n", val);
    }
    return NULL;
}

Key points:

  • Producer waits on c_prod when buffer full; signals c_cons after insert

  • Consumer waits on c_cons when buffer empty; signals c_prod after removal

  • Signal (not broadcast) because one insert/removal enables exactly one counterpart

  • Consumer signals after unlock — safe because signal is unconditional; avoids spurious wakeups