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 executingstart_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 withPTHREAD_CREATE_DETACHEDattribute
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
-lpthreador use-pthreadflag (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 typepthread_mutex_lock(mutex)— acquire (blocks if held)pthread_mutex_unlock(mutex)— releasepthread_mutex_init(mutex, attr)— initialize;NULLattr = 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 typepthread_cond_wait(cond, mutex)— atomically release mutex + block; on wakeup re-acquire mutexpthread_cond_signal(cond)— wake one waiterpthread_cond_broadcast(cond)— wake all waiterspthread_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_prodwhen buffer full; signalsc_consafter insertConsumer waits on
c_conswhen buffer empty; signalsc_prodafter removalSignal (not broadcast) because one insert/removal enables exactly one counterpart
Consumer signals after unlock — safe because signal is unconditional; avoids spurious wakeups